diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..4a7869c7e --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,19 @@ +{ + "name": "Codespace with PowerShell, Pester, Invoke-Build, and .NET 8", + "image": "mcr.microsoft.com/vscode/devcontainers/base:ubuntu", + "features": { + "ghcr.io/devcontainers/features/powershell:1": {}, + "ghcr.io/devcontainers/features/dotnet:1": { + "version": "8.0" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-vscode.powershell", + "pspester.pester-test" + ] + } + }, + "postCreateCommand": "pwsh -Command 'Install-Module -Name InvokeBuild,Pester -Force -SkipPublisherCheck; sleep 5; Invoke-Build Build '" +} diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 9122555d5..b326b5548 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -30,6 +30,7 @@ The following is a set of guidelines for contributing to Pode on GitHub. These a - [Where-Object](#where-object) - [Select-Object](#select-object) - [Measure-Object](#measure-object) + - [New-Object](#new-object) ## Code of Conduct @@ -245,3 +246,15 @@ Instead of using the `Measure-Object` commandlet, please use either the `.Length (@(1, 2, 3)).Length (@{ Name = 'Rick' }).Count ``` + +#### New-Object + +Instead of using the `New-Object` commandlet, please use `::new()` as this is far faster than the former. + +```powershell +# instead of +$stream = New-Object System.IO.MemoryStream + +# do this +$stream = [System.IO.MemoryStream]::new() +``` diff --git a/.github/workflows/PSScriptAnalyzer.yml b/.github/workflows/PSScriptAnalyzer.yml index 8673d21f4..d1af32421 100644 --- a/.github/workflows/PSScriptAnalyzer.yml +++ b/.github/workflows/PSScriptAnalyzer.yml @@ -3,7 +3,7 @@ # separate terms of service, privacy policy, and support # documentation. # -# https://github.com/microsoft/action-psscriptanalyzer +# https://github.com/microsoft/psscriptanalyzer-action # For more information on PSScriptAnalyzer in general, see # https://github.com/PowerShell/PSScriptAnalyzer @@ -48,15 +48,11 @@ jobs: - name: Run PSScriptAnalyzer uses: microsoft/psscriptanalyzer-action@6b2948b1944407914a58661c49941824d149734f with: - # Check https://github.com/microsoft/action-psscriptanalyzer for more info about the options. - # The below set up runs PSScriptAnalyzer to your entire repository and runs some basic security rules. - path: .\ + path: .\src recurse: true - # Include your own basic security rules. Removing this option will run all the rules - includeRule: '"PSAvoidUsingCmdletAliases" ,"PSAvoidUsingPlainTextForPassword","PSAvoidUsingWriteHost","PSAvoidUsingInvokeExpression","PSUseShouldProcessForStateChangingFunctions","PSAvoidUsingUsernameAndPasswordParams","PSUseProcessBlockForPipelineCommand","PSAvoidUsingConvertToSecureStringWithPlainText","PSUseSingularNouns","PSReviewUnusedParameter"' + settings: .\PSScriptAnalyzerSettings.psd1 output: results.sarif - # Upload the SARIF file generated in the previous step - name: Upload SARIF results file uses: github/codeql-action/upload-sarif@v3 with: diff --git a/.github/workflows/ci-no-build-needed.yml b/.github/workflows/ci-no-build-needed.yml index e27e5bafe..3345471f0 100644 --- a/.github/workflows/ci-no-build-needed.yml +++ b/.github/workflows/ci-no-build-needed.yml @@ -12,7 +12,8 @@ on: - 'src/**' - 'tests/**' - '.github/workflows/ci-docs.yml' - - '.github/workflows/ci-pwsh*.yml' + - '.github/workflows/ci-pwsh_lts.yml' + - '.github/workflows/ci-pwsh7_2.yml' - '.github/workflows/ci-powershell.yml' - '.github/workflows/ci-coverage.yml' - '.github/workflows/PSScriptAnalyzer.yml' @@ -30,7 +31,8 @@ on: - 'src/**' - 'tests/**' - '.github/workflows/ci-docs.yml' - - '.github/workflows/ci-pwsh*.yml' + - '.github/workflows/ci-pwsh_lts.yml' + - '.github/workflows/ci-pwsh7_2.yml' - '.github/workflows/ci-powershell.yml' - '.github/workflows/ci-coverage.yml' - '.github/workflows/PSScriptAnalyzer.yml' diff --git a/.github/workflows/ci-pwsh_preview.yml b/.github/workflows/ci-pwsh_preview.yml index 56c9dde71..81bb80c3d 100644 --- a/.github/workflows/ci-pwsh_preview.yml +++ b/.github/workflows/ci-pwsh_preview.yml @@ -28,7 +28,7 @@ env: POWERSHELL_VERSION: 'Preview' jobs: - build: + build-preview: runs-on: ${{ matrix.os }} strategy: diff --git a/.github/workflows/label-issue-project.yml b/.github/workflows/label-issue-project.yml index 3abc4d15e..17b146d85 100644 --- a/.github/workflows/label-issue-project.yml +++ b/.github/workflows/label-issue-project.yml @@ -10,7 +10,7 @@ jobs: name: Add issue to project runs-on: ubuntu-latest steps: - - uses: actions/add-to-project@v1.0.1 + - uses: actions/add-to-project@v1.0.2 with: project-url: https://github.com/users/Badgerati/projects/2 github-token: ${{ secrets.PROJECT_TOKEN }} diff --git a/.github/workflows/open-issue-project.yml b/.github/workflows/open-issue-project.yml index 5928986fc..6a8d134bb 100644 --- a/.github/workflows/open-issue-project.yml +++ b/.github/workflows/open-issue-project.yml @@ -10,7 +10,7 @@ jobs: name: Add issue to project runs-on: ubuntu-latest steps: - - uses: actions/add-to-project@v1.0.1 + - uses: actions/add-to-project@v1.0.2 with: project-url: https://github.com/users/Badgerati/projects/2 github-token: ${{ secrets.PROJECT_TOKEN }} diff --git a/.gitignore b/.gitignore index aa5c0e490..079cb2f3a 100644 --- a/.gitignore +++ b/.gitignore @@ -265,3 +265,4 @@ examples/PetStore/data/PetData.json packers/choco/pode.nuspec packers/choco/tools/ChocolateyInstall.ps1 +docs/Getting-Started/Samples.md diff --git a/.vscode/settings.json b/.vscode/settings.json index 559d3ecf2..6f817660d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,6 +21,7 @@ "powershell.codeFormatting.whitespaceBetweenParameters": false, "powershell.codeFormatting.whitespaceInsideBrace": true, "powershell.scriptAnalysis.settingsPath": "PSScriptAnalyzerSettings.psd1", + "powershell.scriptAnalysis.enable": true, "files.trimTrailingWhitespace": true, "files.associations": { "*.pode": "html" diff --git a/PSScriptAnalyzerSettings.psd1 b/PSScriptAnalyzerSettings.psd1 index 2141ac49d..15486f3d7 100644 --- a/PSScriptAnalyzerSettings.psd1 +++ b/PSScriptAnalyzerSettings.psd1 @@ -1,15 +1,33 @@ -# PSScriptAnalyzerSettings.psd1 @{ - Severity = @('Error', 'Warning', 'Information') + Severity = @('Error', 'Warning', 'Information') + IncludeDefaultRules = $true - Rules = @{ + CustomRulePath = @( + './analyzers/AvoidNewObjectRule.psm1' + ) + + Rules = @{ PSReviewUnusedParameter = @{ CommandsToTraverse = @( - 'Where-Object','Remove-PodeRoute' + 'Where-Object', + 'Remove-PodeRoute' ) } + AvoidNewObjectRule = @{ + Severity = 'Warning' + } } - ExcludeRules = @('PSAvoidUsingCmdletAliases' ,'PSAvoidUsingPlainTextForPassword','PSAvoidUsingWriteHost','PSAvoidUsingInvokeExpression','PSUseShouldProcessForStateChangingFunctions', - 'PSAvoidUsingUsernameAndPasswordParams','PSUseProcessBlockForPipelineCommand','PSAvoidUsingConvertToSecureStringWithPlainText','PSUseSingularNouns','PSReviewUnusedParameter' ) + + ExcludeRules = @( + 'PSAvoidUsingPlainTextForPassword', + 'PSUseShouldProcessForStateChangingFunctions', + 'PSAvoidUsingUsernameAndPasswordParams', + 'PSUseProcessBlockForPipelineCommand', + 'PSAvoidUsingConvertToSecureStringWithPlainText', + 'PSReviewUnusedParameter', + 'PSAvoidAssignmentToAutomaticVariable', + 'PSUseBOMForUnicodeEncodedFile', + 'PSAvoidTrailingWhitespace' + ) } \ No newline at end of file diff --git a/README.md b/README.md index 4d14d2289..3ffdf9f6f 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,16 @@ Pode is a Cross-Platform framework for creating web servers to host [REST APIs](https://badgerati.github.io/Pode/Tutorials/Routes/Overview/), [Web Pages](https://badgerati.github.io/Pode/Tutorials/Routes/Examples/WebPages/), and [SMTP/TCP](https://badgerati.github.io/Pode/Servers/) Servers. Pode also allows you to render dynamic files using [`.pode`](https://badgerati.github.io/Pode/Tutorials/Views/Pode/) files, which are just embedded PowerShell, or other [Third-Party](https://badgerati.github.io/Pode/Tutorials/Views/ThirdParty/) template engines. Plus many more features, including [Azure Functions](https://badgerati.github.io/Pode/Hosting/AzureFunctions/) and [AWS Lambda](https://badgerati.github.io/Pode/Hosting/AwsLambda/) support! -

- -

+```powershell + +Start-PodeServer -ScriptBlock { + Add-PodeEndPoint -Address localhost -port 32005 -Protocol Http + Add-PodeRoute -Method Get -Path '/ping' -ScriptBlock { + Write-PodeJsonResponse -Value @{value = 'pong' } + } +} + +``` See [here](https://badgerati.github.io/Pode/Getting-Started/FirstApp) for building your first app! Don't know HTML, CSS, or JavaScript? No problem! [Pode.Web](https://github.com/Badgerati/Pode.Web) is currently a work in progress, and lets you build web pages using purely PowerShell! @@ -49,8 +56,9 @@ Then navigate to `http://127.0.0.1:8000` in your browser. * Cross-platform using PowerShell Core (with support for PS5) * Docker support, including images for ARM/Raspberry Pi * Azure Functions, AWS Lambda, and IIS support -* OpenAPI, Swagger, and ReDoc support -* Listen on a single or multiple IP address/hostnames +* OpenAPI specification version 3.0.x and 3.1.0 +* OpenAPI documentation with Swagger, Redoc, RapidDoc, StopLight, OpenAPI-Explorer and RapiPdf +* Listen on a single or multiple IP(v4/v6) address/hostnames * Cross-platform support for HTTP(S), WS(S), SSE, SMTP(S), and TCP(S) * Host REST APIs, Web Pages, and Static Content (with caching) * Support for custom error pages @@ -73,6 +81,8 @@ Then navigate to `http://127.0.0.1:8000` in your browser. * Support for File Watchers * In-memory caching, with optional support for external providers (such as Redis) * (Windows) Open the hosted server as a desktop application +* FileBrowsing support +* Localization (i18n) in Arabic, German, Spanish, France, Italian, Japanese, Korean, Polish, Portuguese, and Chinese ## 📦 Install diff --git a/analyzers/AvoidNewObjectRule.psm1 b/analyzers/AvoidNewObjectRule.psm1 new file mode 100644 index 000000000..c6b47f231 --- /dev/null +++ b/analyzers/AvoidNewObjectRule.psm1 @@ -0,0 +1,38 @@ +function Measure-AvoidNewObjectRule { + [CmdletBinding()] + [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.Language.ScriptBlockAst] + $ScriptBlockAst + ) + + # Initialize an empty array to collect diagnostic records + $diagnostics = @() + + try { + # Traverse the AST to find all instances of New-Object cmdlet + $ScriptBlockAst.FindAll({ + param($Ast) + $Ast -is [System.Management.Automation.Language.CommandAst] -and + $Ast.CommandElements[0].Extent.Text -eq 'New-Object' + }, $true) | ForEach-Object { + $diagnostics += [PSCustomObject]@{ + Message = "Avoid using 'New-Object' and use '::new()' instead." + Extent = $_.Extent + RuleName = 'AvoidNewObjectRule' + Severity = 'Warning' + ScriptName = $FileName + } + } + + # Return the diagnostic records + return $diagnostics + } + catch { + $PSCmdlet.ThrowTerminatingError($PSItem) + } +} + +Export-ModuleMember -Function Measure-AvoidNewObjectRule \ No newline at end of file diff --git a/docs/Getting-Started/Debug.md b/docs/Getting-Started/Debug.md index a4647d3c5..0baef9a75 100644 --- a/docs/Getting-Started/Debug.md +++ b/docs/Getting-Started/Debug.md @@ -174,3 +174,75 @@ The steps to attach to the Pode process are as follows: 5. You'll also be able to query variables as well, such as `$WebEvent` and other variables you might have created. 6. When you are done debugging the current request, hit the `d` key. + + + +## Managing Runspace Names + +### Internal Runspace Naming + +In Pode, internal runspaces are automatically assigned distinct names. This built-in naming convention is crucial for identifying and managing runspaces efficiently, particularly during debugging or when monitoring multiple concurrent processes. + +Pode uses specific naming patterns for its internal runspaces, which include: + +- **Pode_Web_Listener_1** +- **Pode_Signals_Broadcaster_1** +- **Pode_Signals_Listener_1** +- **Pode_Web_KeepAlive_1** +- **Pode_Files_Watcher_1** +- **Pode_Main_Logging_1** +- **Pode_Timers_Scheduler_1** +- **Pode_Schedules_[Schedule Name]_1** – where `[Schedule Name]` is the name of the schedule. +- **Pode_Tasks_[Task Name]_1** – where `[Task Name]` is the name of the task. + +These default names are automatically assigned by Pode, making it easier to identify the purpose of each runspace during execution. + +### Customizing Runspace Names + +By default, Pode’s Tasks, Schedules, and Timers label their associated runspaces with their own names (as shown above). This simplifies the identification of runspaces when debugging or reviewing logs. + +However, if a different runspace name is needed, Pode allows you to customize it. Inside the script block of `Add-PodeTask`, `Add-PodeSchedule`, or `Add-PodeTimer`, you can use the `Set-PodeCurrentRunspaceName` cmdlet to assign any custom name you prefer. + +```powershell +Set-PodeCurrentRunspaceName -Name 'Custom Runspace Name' +``` + +This cmdlet sets a custom name for the runspace, making it easier to track during execution. + +#### Example + +Here’s an example that demonstrates how to set a custom runspace name in a Pode task: + +```powershell +Add-PodeTask -Name 'Test2' -ScriptBlock { + param($value) + # Set a custom name for the current runspace + Set-PodeCurrentRunspaceName -Name 'My Fancy Runspace' + Start-Sleep -Seconds 10 + "A $($value) is never late, it arrives exactly when it means to" | Out-Default +} +``` + +In this example, the `Set-PodeCurrentRunspaceName` cmdlet assigns the custom name `'My Fancy Runspace'` to the task's runspace. This is especially useful for debugging or when examining logs, as the custom name makes the runspace more recognizable. + +### Retrieving Runspace Names + +Pode also provides the `Get-PodeCurrentRunspaceName` cmdlet to retrieve the name of the current runspace. This is particularly helpful when you need to log or display the runspace name dynamically during execution. + +```powershell +Get-PodeCurrentRunspaceName +``` + +This cmdlet returns the name of the current runspace, allowing for easier tracking and management in complex scenarios with multiple concurrent runspaces. + +#### Example + +Here’s an example that uses `Get-PodeCurrentRunspaceName` to output the runspace name during the execution of a schedule: + +```powershell +Add-PodeSchedule -Name 'TestSchedule' -Cron '@hourly' -ScriptBlock { + Write-PodeHost "Runspace name: $(Get-PodeCurrentRunspaceName)" +} +``` + +In this example, the schedule outputs the name of the runspace executing the script block every hour. This can be useful for logging and monitoring purposes when dealing with multiple schedules or tasks. diff --git a/docs/Getting-Started/FirstApp.md b/docs/Getting-Started/FirstApp.md index df2cdc0da..c1e52ceab 100644 --- a/docs/Getting-Started/FirstApp.md +++ b/docs/Getting-Started/FirstApp.md @@ -36,6 +36,16 @@ Success, saved package.json Import-Module -Name Pode -MaximumVersion 2.99.99 ``` +* To ensure that any errors during the import process are caught and handled appropriately, use a try-catch block: + +```powershell +try { + Import-Module -Name 'Pode' -MaximumVersion 2.99.99 -ErrorAction Stop +} catch { + # exception management code +} +``` + * Within your `server.ps1` file, first you need to start the Server. This is where the main script will go that defines how the server should function: ```powershell diff --git a/docs/Getting-Started/GitHubCodespace.md b/docs/Getting-Started/GitHubCodespace.md new file mode 100644 index 000000000..78fa890de --- /dev/null +++ b/docs/Getting-Started/GitHubCodespace.md @@ -0,0 +1,55 @@ + +# GitHub Codespace and Pode + +GitHub Codespaces provides a cloud-based development environment directly integrated with GitHub. This allows you to set up your development environment with pre-configured settings, tools, and extensions. In this guide, we will walk you through using GitHub Codespace to work with Pode, a web framework for building web applications and APIs in PowerShell. + +## Prerequisites + +- A GitHub account +- A repository set up for your Pode project, including the `devcontainer.json` configuration file. + +## Launching GitHub Codespace + +1. **Open GitHub Codespace:** + + Go to your GitHub repository on the web. Click on the green `Code` button, and then select `Open with Codespaces`. If you don't have any Codespaces created, you can create a new one by clicking `New codespace`. + +2. **Codespace Initialization:** + + Once the Codespace is created, it will use the existing `devcontainer.json` configuration to set up the environment. This includes installing the necessary VS Code extensions and PowerShell modules specified in the configuration. + +3. **Verify the Setup:** + + - The terminal in the Codespace will default to PowerShell (`pwsh`). + - Check that the required PowerShell modules are installed by running: + + ```powershell + Get-Module -ListAvailable + ``` + + You should see `InvokeBuild` and `Pester` listed among the available modules. + +## Running a Pode Application + +1. **Use an Example Pode Project:** + + Pode comes with several examples in the `examples` folder. You can run one of these examples to verify that your setup is working. For instance, let's use the `HelloWorld` example. + +2. **Open HelloWorld** + + Navigate to the `examples/HelloWorld` directory and open the `HelloWorld.ps1` file + +3. **Run the sample** + + Run the Pode server by executing the `HelloWorld.ps1` script in the PowerShell terminal: + + ```powershell + ./examples/HelloWorld/HelloWorld.ps1 + ``` + or using the `Run/Debug` on the UI + +4. **Access the Pode Application:** + + Once the Pode server is running, you can access your Pode application by navigating to the forwarded port provided by GitHub Codespaces. This is usually indicated by a URL in the terminal or in the Codespaces interface. + +For more information on using Pode and its features, refer to the [Pode documentation](https://badgerati.github.io/Pode/). diff --git a/docs/Getting-Started/KnownIssues.md b/docs/Getting-Started/KnownIssues.md index 9ceee09a5..c4ca375ea 100644 --- a/docs/Getting-Started/KnownIssues.md +++ b/docs/Getting-Started/KnownIssues.md @@ -18,13 +18,30 @@ New-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Services\HTTP\Parameters' ## PowerShell Classes -Pode uses Runspaces to deal with multithreading and other background tasks. Due to this, PowerShell classes do not work as intended and are unsafe to use. +Pode utilizes Runspaces for multithreading and other background tasks, which makes PowerShell classes behave unpredictably and renders them unsafe to use. This is primarily because an instance of a class created in one Runspace will always be marshaled back to the original Runspace whenever it is accessed again, potentially causing Routes and Middleware to become contaminated. -You can find more information about this issue [here on PowerShell](https://github.com/PowerShell/PowerShell/issues/3651). +For more details on this issue, you can refer to the [PowerShell GitHub issue](https://github.com/PowerShell/PowerShell/issues/3651). -The crux of the issue is that if you create an instance of a class in one Runspace, then every time you try to use that instance again it will always be marshaled back to the original Runspace. This means Routes and Middleware can become contaminated. +To avoid these problems, it is recommended to use Hashtables or PSObjects instead. -It's recommended to switch to either Hashtables or PSObjects, but if you need to use classes then the following should let classes work: +However, if you need to use classes, PowerShell 7.4 introduces the `[NoRunspaceAffinity()]` attribute that makes classes thread-safe by solving this issue. + +Here's an example of a class definition with the `[NoRunspaceAffinity()]` attribute: + +```powershell +# Class definition with NoRunspaceAffinity attribute +[NoRunspaceAffinity()] +class SafeClass { + static [object] ShowRunspaceId($val) { + return [PSCustomObject]@{ + ThreadId = [Threading.Thread]::CurrentThread.ManagedThreadId + RunspaceId = [runspace]::DefaultRunspace.Id + } + } +} +``` + +If you need to support versions prior to PowerShell 7.4, you can use the following approach: * Create a module (CreateClassInstanceHelper.psm1) with the content: diff --git a/docs/Servers/TCP.md b/docs/Servers/TCP.md index 7a2acd44a..10425897c 100644 --- a/docs/Servers/TCP.md +++ b/docs/Servers/TCP.md @@ -172,14 +172,14 @@ Start-PodeServer { Verbs will be passed the `$TcpEvent` object, that contains the Request, Response, and other properties: -| Name | Type | Description | -| ---- | ---- | ----------- | -| Request | object | The raw Request object | -| Response | object | The raw Response object | -| Lockable | hashtable | A synchronized hashtable that can be used with `Lock-PodeObject` | -| Endpoint | hashtable | Contains the Address and Protocol of the endpoint being hit - such as "pode.example.com" or "127.0.0.2", or HTTP or HTTPS for the Protocol | -| Parameters | hashtable | Contains the parsed parameter values from the Verb's path | -| Timestamp | datetime | The current date and time of the Request | +| Name | Type | Description | +| ---------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| Request | object | The raw Request object | +| Response | object | The raw Response object | +| Lockable | hashtable | A synchronized hashtable that can be used with `Lock-PodeObject` | +| Endpoint | hashtable | Contains the Address and Protocol of the endpoint being hit - such as "pode.example.com" or "127.0.0.2", or HTTP or HTTPS for the Protocol | +| Parameters | hashtable | Contains the parsed parameter values from the Verb's path | +| Timestamp | datetime | The current date and time of the Request | ## Test Send @@ -189,11 +189,11 @@ The following function can be used to test sending messages to a TCP server. Thi function Send-TCPMessage($Endpoint, $Port, $Message) { # Setup connection $Address = [System.Net.IPAddress]::Parse([System.Net.Dns]::GetHostAddresses($EndPoint)) - $Socket = New-Object System.Net.Sockets.TCPClient($Address,$Port) + $Socket = [System.Net.Sockets.TcpClient]::new($Address, $Port) - # Setup stream wrtier + # Setup stream writer $Stream = $Socket.GetStream() - $Writer = New-Object System.IO.StreamWriter($Stream) + $Writer = [System.IO.StreamWriter]::new($Stream) # Write message to stream $Writer.WriteLine($Message) diff --git a/docs/Tutorials/Authentication/Inbuilt/UserFile.md b/docs/Tutorials/Authentication/Inbuilt/UserFile.md index 564c54dbc..4b9109809 100644 --- a/docs/Tutorials/Authentication/Inbuilt/UserFile.md +++ b/docs/Tutorials/Authentication/Inbuilt/UserFile.md @@ -20,14 +20,14 @@ The default users file is `./users.json` at the root of the server. You can supp The users file is a JSON array of user objects, each user object must contain the following (metadata is optional): -| Name | Type | Description | -| ---- | ---- | ----------- | -| Username | string | The user's username | -| Name | string | The user's fullname | -| Email | string | The user's email address | -| Password | string | Either a SHA256 or an HMAC SHA256 of the user's password | -| Groups | string[] | An array of groups which the the user is a member | -| Metadata | psobject | Custom metadata for the user | +| Name | Type | Description | +| -------- | -------- | -------------------------------------------------------- | +| Username | string | The user's username | +| Name | string | The user's fullname | +| Email | string | The user's email address | +| Password | string | Either a SHA256 or an HMAC SHA256 of the user's password | +| Groups | string[] | An array of groups which the the user is a member | +| Metadata | psobject | Custom metadata for the user | For example: @@ -66,7 +66,7 @@ Regardless of whether the password is a standard SHA256 hash or HMAC hash, the h ```powershell function ConvertTo-SHA256([string]$String) { - $SHA256 = New-Object System.Security.Cryptography.SHA256Managed + $SHA256 = [System.Security.Cryptography.SHA256Managed]::new() $SHA256Hash = $SHA256.ComputeHash([Text.Encoding]::ASCII.GetBytes($String)) $SHA256HashString = [Convert]::ToBase64String($SHA256Hash) return $SHA256HashString @@ -77,7 +77,7 @@ function ConvertTo-SHA256([string]$String) ```powershell function ConvertTo-HMACSHA256([string]$String, [string]$Secret) { - $HMACSHA256 = New-Object System.Security.Cryptography.HMACSHA256 + $HMACSHA256 = [System.Security.Cryptography.HMACSHA256]::new() $HMACSHA256.Secret = [Text.Encoding]::ASCII.GetBytes($Secret) $HMACSHA256Hash = $HMACSHA256.ComputeHash([Text.Encoding]::ASCII.GetBytes($String)) $HMACSHA256HashString = [Convert]::ToBase64String($HMACSHA256Hash) @@ -89,13 +89,13 @@ function ConvertTo-HMACSHA256([string]$String, [string]$Secret) { The User object returned, and accessible on Routes, and other functions via the [web event](../../../WebEvent)'s `$WebEvent.Auth.User` property, will contain the following information: -| Name | Type | Description | -| ---- | ---- | ----------- | -| Username | string | The user's username | -| Name | string | The user's fullname | -| Email | string | The user's email address | -| Groups | string[] | An array of groups which the the user is a member | -| Metadata | psobject | Custom metadata for the user | +| Name | Type | Description | +| -------- | -------- | ------------------------------------------------- | +| Username | string | The user's username | +| Name | string | The user's fullname | +| Email | string | The user's email address | +| Groups | string[] | An array of groups which the the user is a member | +| Metadata | psobject | Custom metadata for the user | Such as: diff --git a/docs/Tutorials/Basics.md b/docs/Tutorials/Basics.md index 482ba68ae..137aa8b29 100644 --- a/docs/Tutorials/Basics.md +++ b/docs/Tutorials/Basics.md @@ -1,15 +1,25 @@ # Basics + !!! Warning +You can initiate only one server per PowerShell instance. To run multiple servers, start additional PowerShell, or pwsh, sessions. Each session can run its own server. This is fundamental to how Pode operates, so consider it when designing your scripts and infrastructure. -!!! warning - You can only start one server in your script - -Although not required, it is recommended to import the Pode module using a maximum version, to avoid any breaking changes from new major versions: +While it’s not mandatory, we strongly recommend importing the Pode module with a specified maximum version. This practice helps to prevent potential issues arising from breaking changes introduced in new major versions: ```powershell Import-Module -Name Pode -MaximumVersion 2.99.99 ``` +To further enhance the robustness of your code, consider wrapping the import statement within a try/catch block. This way, if the module fails to load, your script won’t proceed, preventing possible errors or unexpected behavior: + +```powershell +try { + Import-Module -Name Pode -MaximumVersion 2.99.99 +} catch { + Write-Error "Failed to load the Pode module" + throw +} +``` + The script for your server should be set in the [`Start-PodeServer`](../../Functions/Core/Start-PodeServer) function, via the `-ScriptBlock` parameter. The following example will listen over HTTP on port 8080, and expose a simple HTML page of running processes at `http://localhost:8080/processes`: ```powershell @@ -72,3 +82,29 @@ PS> Start-PodeServer -FilePath './File.ps1' !!! tip Normally when you restart your Pode server any changes to the main scriptblock don't reflect. However, if you reference a file instead, then restarting the server will reload the scriptblock from that file - so any changes will reflect. + +## Internationalization + +Pode has built-in support for internationalization (i18n). By default, Pode uses the `$PsUICulture` variable to determine the User Interface Culture (UICulture). + +You can enforce a specific localization when importing the Pode module by using the UICulture argument. This argument accepts a culture code, which specifies the language and regional settings to use. + +Here’s an example of how to enforce Korean localization: + +```powershell +Import-Module -Name Pode -ArgumentList 'ko-KR' +``` + +In this example, 'ko-KR' is the culture code for Korean as used in South Korea. You can replace 'ko-KR' with the culture code for any other language or region. + +As an alternative to specifying the UICulture when importing the Pode module, you can also change the UICulture within the PowerShell environment itself. + +This can be done using the following command: + +```powershell +[System.Threading.Thread]::CurrentThread.CurrentUICulture = 'ko-KR' +``` + +This command changes the UICulture for the current PowerShell session to Korean as used in South Korea. + +Please note that this change is temporary and will only affect the current session. If you open a new PowerShell session, it will use the default UICulture. \ No newline at end of file diff --git a/docs/Tutorials/CORS.md b/docs/Tutorials/CORS.md new file mode 100644 index 000000000..b4496cb55 --- /dev/null +++ b/docs/Tutorials/CORS.md @@ -0,0 +1,93 @@ + +# CORS + +## What is CORS? +Cross-Origin Resource Sharing (CORS) is a security feature implemented by web browsers to restrict web pages from making requests to a different domain than the one that served the web page. This is a critical aspect of web security, helping to prevent malicious sites from accessing sensitive data from another domain. + +## CORS Challenges +When developing web applications, you may encounter situations where your web page needs to request resources from a different domain. This can lead to CORS errors if the appropriate headers are not set to allow these cross-origin requests. Common challenges include: +- Handling pre-flight requests. +- Allowing specific methods and headers. +- Managing credentials in cross-origin requests. +- Setting the appropriate origins. + +## Addressing CORS Challenges + +Pode simplifies handling CORS by providing the `Set-PodeSecurityAccessControl` function, which allows you to define the necessary headers to manage cross-origin requests effectively. + +### Key Headers for CORS + +1. **Access-Control-Allow-Origin**: Specifies which origins are permitted to access the resource. +2. **Access-Control-Allow-Methods**: Lists the HTTP methods that are allowed when accessing the resource. +3. **Access-Control-Allow-Headers**: Indicates which HTTP headers can be used during the actual request. +4. **Access-Control-Max-Age**: Specifies how long the results of a pre-flight request can be cached. +5. **Access-Control-Allow-Credentials**: Indicates whether credentials are allowed in the request. + +### Setting CORS Headers instead + +The `Set-PodeSecurityAccessControl` function allows you to set these headers easily. Here’s how you can address common CORS challenges using this function: + +1. **Allowing All Origins** + ```powershell + Set-PodeSecurityAccessControl -Origin '*' + ``` + This sets the `Access-Control-Allow-Origin` header to allow requests from any origin. + +2. **Specifying Allowed Methods** + ```powershell + Set-PodeSecurityAccessControl -Methods 'GET', 'POST', 'OPTIONS' + ``` + This sets the `Access-Control-Allow-Methods` header to allow only the specified methods. + +3. **Specifying Allowed Headers** + ```powershell + Set-PodeSecurityAccessControl -Headers 'Content-Type', 'Authorization' + ``` + This sets the `Access-Control-Allow-Headers` header to allow the specified headers. + +4. **Handling Credentials** + ```powershell + Set-PodeSecurityAccessControl -Credentials + ``` + This sets the `Access-Control-Allow-Credentials` header to allow credentials in requests. + +5. **Setting Cache Duration for Pre-flight Requests** + ```powershell + Set-PodeSecurityAccessControl -Duration 3600 + ``` + This sets the `Access-Control-Max-Age` header to cache the pre-flight request for one hour. + +6. **Automatic Header and Method Detection** + ```powershell + Set-PodeSecurityAccessControl -AutoHeaders -AutoMethods + ``` + These parameters automatically populate the list of allowed headers and methods based on your OpenApi definition and defined routes, respectively. + +7. **Enabling Global OPTIONS Route** + ```powershell + Set-PodeSecurityAccessControl -WithOptions + ``` + This creates a global OPTIONS route to handle pre-flight requests automatically. + +8. **Additional Security with Cross-Domain XHR Requests** + ```powershell + Set-PodeSecurityAccessControl -CrossDomainXhrRequests + ``` + This adds the 'x-requested-with' header to the list of allowed headers, enhancing security. + +### Example Configuration + +Here is an example of configuring CORS settings in Pode using `Set-PodeSecurityAccessControl`: + +```powershell +Set-PodeSecurityAccessControl -Origin 'https://example.com' -Methods 'GET', 'POST' -Headers 'Content-Type', 'Authorization' -Duration 7200 -Credentials -WithOptions -AutoHeaders -AutoMethods -CrossDomainXhrRequests +``` + +This example sets up CORS to allow requests from `https://example.com`, allows `GET` and `POST` methods, permits `Content-Type` and `Authorization` headers, enables credentials, caches pre-flight requests for two hours, automatically detects headers and methods, and allows cross-domain XHR requests. + +### More Information on CORS + +For more information on CORS, you can refer to the following resources: +- [Fetch Living Standard](https://fetch.spec.whatwg.org/) +- [CORS in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/security/cors?view=aspnetcore-7.0#credentials-in-cross-origin-requests) +- [MDN Web Docs on CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) diff --git a/docs/Tutorials/Compression/Requests.md b/docs/Tutorials/Compression/Requests.md index 804cea5e8..f5604a307 100644 --- a/docs/Tutorials/Compression/Requests.md +++ b/docs/Tutorials/Compression/Requests.md @@ -94,8 +94,8 @@ $message = ($data | ConvertTo-Json) $bytes = [System.Text.Encoding]::UTF8.GetBytes($message) # compress the message using gzip -$ms = New-Object -TypeName System.IO.MemoryStream -$gzip = New-Object System.IO.Compression.GZipStream($ms, [IO.Compression.CompressionMode]::Compress, $true) +$ms = [System.IO.MemoryStream]::new() +$gzip = [System.IO.Compression.GZipStream]::new($ms, [IO.Compression.CompressionMode]::Compress, $true) $gzip.Write($bytes, 0, $bytes.Length) $gzip.Close() $ms.Position = 0 diff --git a/docs/Tutorials/Configuration.md b/docs/Tutorials/Configuration.md index 89c59d724..6b18e04fa 100644 --- a/docs/Tutorials/Configuration.md +++ b/docs/Tutorials/Configuration.md @@ -68,19 +68,22 @@ A "path" like `Server.Ssl.Protocols` looks like the below in the file: } ``` -| Path | Description | Docs | -| -------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------ | -| Server.Ssl.Protocols | Indicates the SSL Protocols that should be used | [link](../Certificates) | -| Server.Request | Defines request timeout and maximum body size | [link](../RequestLimits) | -| Server.AutoImport | Defines the AutoImport scoping rules for Modules, SnapIns and Functions | [link](../Scoping) | -| Server.Logging | Defines extra configuration for Logging, like masking sensitive data | [link](../Logging/Overview) | -| Server.Root | Overrides root path of the server | [link](../Misc/ServerRoot) | -| Server.Restart | Defines configuration for automatically restarting the server | [link](../Restarting/Types/AutoRestarting) | -| Server.FileMonitor | Defines configuration for restarting the server based on file updates | [link](../Restarting/Types/FileMonitoring) | -| Web.OpenApi.DefaultDefinitionTag | Define the primary tag name for OpenAPI ( 'default' is the default) | [link](../OpenAPI/OpenAPI) | -| Web.Static.ValidateLast | Changes the way routes are processed. | [link](../Routes/Utilities/StaticContent) | -| Web.TransferEncoding | Sets the Request TransferEncoding | [link](../Compression/Requests) | -| Web.Compression | Sets any compression to use on the Response | [link](../Compression/Responses) | -| Web.ContentType | Define expected Content Types for certain Routes | [link](../Routes/Utilities/ContentTypes) | -| Web.ErrorPages | Defines configuration for custom error pages | [link](../Routes/Utilities/ErrorPages) | -| Web.Static | Defines configuration for static content, such as caching | [link](../Routes/Utilities/StaticContent) | +| Path | Description | Docs | +| -------------------------------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------------- | +| Server.Ssl.Protocols | Indicates the SSL Protocols that should be used | [link](../Certificates) | +| Server.Request | Defines request timeout and maximum body size | [link](../RequestLimits) | +| Server.AutoImport | Defines the AutoImport scoping rules for Modules, SnapIns and Functions | [link](../Scoping) | +| Server.Logging | Defines extra configuration for Logging, like masking sensitive data | [link](../Logging/Overview) | +| Server.Root | Overrides root path of the server | [link](../Misc/ServerRoot) | +| Server.Restart | Defines configuration for automatically restarting the server | [link](../Restarting/Types/AutoRestarting) | +| Server.FileMonitor | Defines configuration for restarting the server based on file updates | [link](../Restarting/Types/FileMonitoring) | +| Server.ReceiveTimeout | Define the amount of time a Receive method call will block waiting for data | [link](../Endpoints/Basic/StaticContent/#server-timeout) | +| Server.DefaultFolders | Set the Default Folders paths | [link](../Routes/Utilities/StaticContent/#changing-the-default-folders) | +| Web.OpenApi.DefaultDefinitionTag | Define the primary tag name for OpenAPI ( `default` is the default) | [link](../OpenAPI/Overview) | +| Web.OpenApi.UsePodeYamlInternal | Force the use of the internal YAML converter (`False` is the default) | | +| Web.Static.ValidateLast | Changes the way routes are processed. | [link](../Routes/Utilities/StaticContent) | +| Web.TransferEncoding | Sets the Request TransferEncoding | [link](../Compression/Requests) | +| Web.Compression | Sets any compression to use on the Response | [link](../Compression/Responses) | +| Web.ContentType | Define expected Content Types for certain Routes | [link](../Routes/Utilities/ContentTypes) | +| Web.ErrorPages | Defines configuration for custom error pages | [link](../Routes/Utilities/ErrorPages) | +| Web.Static | Defines configuration for static content, such as caching | [link](../Routes/Utilities/StaticContent) | \ No newline at end of file diff --git a/docs/Tutorials/Endpoints/Basics.md b/docs/Tutorials/Endpoints/Basics.md index f4dbd93f8..888a529ba 100644 --- a/docs/Tutorials/Endpoints/Basics.md +++ b/docs/Tutorials/Endpoints/Basics.md @@ -174,3 +174,17 @@ The following is the structure of the Endpoint object internally, as well as the | Protocol | string | The protocol of the Endpoint. Such as: HTTP, HTTPS, WS, etc. | | Type | string | The type of the Endpoint. Such as: HTTP, WS, SMTP, TCP | | Certificate | hashtable | Details about the certificate that will be used for SSL Endpoints | + +## Server timeout + +The timeout is configurable using the `ReceiveTimeout` property in the `server.psd1` configuration file, the amount of time is in milliseconds. The meaning is that the server will wait for data to be received before timing out. This is useful for controlling how long the server should wait during data reception operations, enhancing the performance and responsiveness. + +To set this property, include it in `server.psd1` configuration file as shown below: + +```powershell +@{ + Server = @{ + ReceiveTimeout = 5000 # Timeout in milliseconds + } +} +``` diff --git a/docs/Tutorials/Middleware/Types/Sessions.md b/docs/Tutorials/Middleware/Types/Sessions.md index ba622c0e2..90221f1dc 100644 --- a/docs/Tutorials/Middleware/Types/Sessions.md +++ b/docs/Tutorials/Middleware/Types/Sessions.md @@ -111,7 +111,7 @@ For example, the following is a mock up of a Storage for Redis. Note that the fu ```powershell # create the object -$store = New-Object -TypeName psobject +$store = [psobject]::new() # add a Get property for retreiving a session's data by SessionId $store | Add-Member -MemberType NoteProperty -Name Get -Value { diff --git a/docs/Tutorials/OpenAPI/1Overview.md b/docs/Tutorials/OpenAPI/1Overview.md new file mode 100644 index 000000000..65cf5ebe6 --- /dev/null +++ b/docs/Tutorials/OpenAPI/1Overview.md @@ -0,0 +1,312 @@ +# Overview + +Pode has built-in support for converting your routes into OpenAPI 3.0 definitions. There is also support for enabling simple Swagger and/or ReDoc viewers and others. + +The OpenApi module has been extended with many more functions, and some old ones have been improved. + +For more detailed information regarding OpenAPI and Pode, please refer to [OpenAPI Specification and Pode](../Specification/v3_0_3.md) + +You can enable OpenAPI in Pode, and a straightforward definition will be generated. However, to get a more complex definition with request bodies, parameters, and response payloads, you'll need to use the relevant OpenAPI functions detailed below. + +## Enabling OpenAPI + +To enable support for generating OpenAPI definitions you'll need to use the [`Enable-PodeOpenApi`](../../../Functions/OpenApi/Enable-PodeOpenApi) function. This will allow you to set a title and version for your API. You can also set a default route to retrieve the OpenAPI definition for tools like Swagger or ReDoc, the default is at `/openapi`. + +You can also set a route filter (such as `/api/*`, the default is `/*` for everything), so only those routes are included in the definition. + +An example of enabling OpenAPI is a follows: + +```powershell +Enable-PodeOpenApi -Title 'My Awesome API' -Version 9.0.0.1 +``` + +An example of setting the OpenAPI route is a follows. This will create a route accessible at `/docs/openapi`: + +```powershell +Enable-PodeOpenApi -Path '/docs/openapi' -Title 'My Awesome API' -Version 9.0.0.1 +``` + +### Default Setup + +In the very simplest of scenarios, just enabling OpenAPI will generate a minimal definition. It can be viewed in Swagger/ReDoc etc, but won't be usable for trying calls. + +When you enable OpenAPI, and don't set any other OpenAPI data, the following is the minimal data that is included: + +* Every route will have a 200 and Default response +* Although routes will be included, no request bodies, parameters or response payloads will be defined +* If you have multiple endpoints, then the servers section will be included +* Any authentication will be included + +This can be changed with [`Enable-PodeOpenApi`](../../../Functions/OpenApi/Enable-PodeOpenApi) + +For example to change the default response 404 and 500 + +```powershell +Enable-PodeOpenApi -Path '/docs/openapi' -OpenApiVersion '3.0.3' -DefaultResponses ( + New-PodeOAResponse -StatusCode 404 -Description 'User not found' | Add-PodeOAResponse -StatusCode 500 + ) +``` + +For disabling the Default Response use: + +```powershell +Enable-PodeOpenApi -Path '/docs/openapi' -OpenApiVersion '3.0.3' -NoDefaultResponses +``` + +For disabling the Minimal Definitions feature use: + +```powershell +Enable-PodeOpenApi -Path '/docs/openapi' -OpenApiVersion '3.0.3' -DisableMinimalDefinitions +``` + +### Get Definition + +Instead of defining a route to return the definition, you can write the definition to the response whenever you want, and in any route, using the [`Get-PodeOADefinition`](../../../Functions/OpenApi/Get-PodeOADefinition) function. This could be useful in certain scenarios like in Azure Functions, where you can enable OpenAPI, and then write the definition to the response of a GET request if some query parameter is set; eg: `?openapi=1`. + +For example: + +```powershell +Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + if ($WebEvent.Query.openapi -eq 1) { + Get-PodeOpenApiDefinition | Write-PodeJsonResponse + } +} +``` + +## OpenAPI Info object + +In previous releases some of the Info object properties like Version and Title were defined by [`Enable-PodeOpenApi`](../../../Functions/OpenApi/Enable-PodeOpenApi). +Starting from version 2.10 a new [`Add-PodeOAInfo`](../../../Functions/OpenApi/Add-PodeOAInfo) function has been added to create a full OpenAPI Info spec. + +```powershell +Add-PodeOAInfo -Title 'Swagger Petstore - OpenAPI 3.0' ` + -Version 1.0.17 ` + -Description $InfoDescription ` + -TermsOfService 'http://swagger.io/terms/' ` + -LicenseName 'Apache 2.0' ` + -LicenseUrl 'http://www.apache.org/licenses/LICENSE-2.0.html' ` + -ContactName 'API Support' ` + -ContactEmail 'apiteam@swagger.io' +``` + +## OpenAPI configuration Best Practice + +Pode is rich of functions to create and configure an complete OpenApi spec. Here is a typical code you should use to initiate an OpenApi spec + +```powershell +#Initialize OpenApi +Enable-PodeOpenApi -Path '/docs/openapi' -Title 'Swagger Petstore - OpenAPI 3.0' ` + -OpenApiVersion 3.1 -DisableMinimalDefinitions -NoDefaultResponses + +# OpenApi Info +Add-PodeOAInfo -Title 'Swagger Petstore - OpenAPI 3.0' ` + -Version 1.0.17 ` + -Description 'This is a sample Pet Store Server based on the OpenAPI 3.0 specification. ...' ` + -TermsOfService 'http://swagger.io/terms/' ` + -LicenseName 'Apache 2.0' ` + -LicenseUrl 'http://www.apache.org/licenses/LICENSE-2.0.html' ` + -ContactName 'API Support' ` + -ContactEmail 'apiteam@swagger.io' ` + -ContactUrl 'http://example.com/support' + +# Endpoint for the API + Add-PodeOAServerEndpoint -url '/api/v3.1' -Description 'default endpoint' + + # OpenApi external documentation links + $extDoc = New-PodeOAExternalDoc -Name 'SwaggerDocs' -Description 'Find out more about Swagger' -Url 'http://swagger.io' + $extDoc | Add-PodeOAExternalDoc + + # OpenApi documentation viewer + Enable-PodeOAViewer -Type Swagger -Path '/docs/swagger' + Enable-PodeOAViewer -Type ReDoc -Path '/docs/redoc' + Enable-PodeOAViewer -Type RapiDoc -Path '/docs/rapidoc' + Enable-PodeOAViewer -Type StopLight -Path '/docs/stoplight' + Enable-PodeOAViewer -Type Explorer -Path '/docs/explorer' + Enable-PodeOAViewer -Bookmarks -Path '/docs' +``` + +## Authentication + +Any authentication defined, either by [`Add-PodeAuthMiddleware`](../../../Functions/Authentication/Add-PodeAuthMiddleware), or using the `-Authentication` parameter on Routes, will be automatically added to the `security` section of the OpenAPI definition. + + +## Tags + +In OpenAPI, a "tag" is used to group related operations. Tags are often used to organize and categorize endpoints in an API specification, making it easier to understand and navigate the API documentation. Each tag can be associated with one or more API operations, and these tags are then used in tools like Swagger UI to group and display operations in a more organized way. + +Here's an example of how to define and use tags: + +```powershell +# create an External Doc reference +$swaggerDocs = New-PodeOAExternalDoc -Description 'Find out more about Swagger' -Url 'http://swagger.io' + +# create a Tag +Add-PodeOATag -Name 'pet' -Description 'Everything about your Pets' -ExternalDoc $swaggerDocs + +Add-PodeRoute -PassThru -Method get -Path '/pet/findByStatus' -Authentication 'Login-OAuth2' -Scope 'read' -AllowAnon -ScriptBlock { + #route code +} | Set-PodeOARouteInfo -Summary 'Finds Pets by status' -Description 'Multiple status values can be provided with comma-separated strings' ` + -Tags 'pet' -OperationId 'findPetsByStatus' +``` + +## Routes + +To extend the definition of a route, you can use the `-PassThru` switch on the [`Add-PodeRoute`](../../../Functions/Routes/Add-PodeRoute) function. This will cause the route that was created to be returned, so you can pass it down the pipe into more OpenAPI functions. + +To add metadata to a route's definition you can use the [`Set-PodeOARouteInfo`](../../../Functions/OpenApi/Set-PodeOARouteInfo) function. This will allow you to define a summary/description for the route, as well as tags for grouping: + +```powershell +Add-PodeRoute -Method Get -Path "/api/resources" -ScriptBlock { + Set-PodeResponseStatus -Code 200 +} -PassThru | + Set-PodeOARouteInfo -Summary 'Retrieve some resources' -Tags 'Resources' +``` + +Each of the following OpenAPI functions have a `-PassThru` switch, allowing you to chain many of them together. + +### Responses + +You can define multiple responses for a route, but only one of each status code, using the [`Add-PodeOAResponse`](../../../Functions/OpenApi/Add-PodeOAResponse) function. You can either just define the response and status code, with a custom description, or with a schema defining the payload of the response. + +The following is an example of defining simple 200 and 404 responses on a route: + +```powershell +Add-PodeRoute -Method Get -Path "/api/user/:userId" -ScriptBlock { + # logic +} -PassThru | + Add-PodeOAResponse -StatusCode 200 -PassThru | + Add-PodeOAResponse -StatusCode 404 -Description 'User not found' +``` + +Whereas the following is a more complex definition, which also defines the responses JSON payload. This payload is defined as an object with a string Name, and integer UserId: + +```powershell +Add-PodeRoute -Method Get -Path '/api/users/:userId' -ScriptBlock { + Write-PodeJsonResponse -Value @{ + Name = 'Rick' + UserId = $WebEvent.Parameters['userId'] + } +} -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'A user object' --Content @{ + 'application/json' = (New-PodeOAStringProperty -Name 'Name'| + New-PodeOAIntProperty -Name 'UserId'| New-PodeOAObjectProperty) + } +``` + +the JSON response payload defined is as follows: + +```json +{ + "Name": [string], + "UserId": [integer] +} +``` + +In case the response JSON payload is an array + +```powershell +Add-PodeRoute -Method Get -Path '/api/users/:userId' -ScriptBlock { + Write-PodeJsonResponse -Value @{ + Name = 'Rick' + UserId = $WebEvent.Parameters['userId'] + } + } -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'A user object' -Content ( + New-PodeOAContentMediaType -ContentType 'application/json' -Array -Content ( + New-PodeOAStringProperty -Name 'Name' | + New-PodeOAIntProperty -Name 'UserId' | + New-PodeOAObjectProperty + ) + ) +``` + +```json +[ + { + "Name": [string], + "UserId": [integer] + } +] +``` + +Internally, each route is created with an empty default 200 and 500 response. You can remove these, or other added responses, by using [`Remove-PodeOAResponse`](../../../Functions/OpenApi/Remove-PodeOAResponse): + +```powershell +Add-PodeRoute -Method Get -Path "/api/user/:userId" -ScriptBlock { + # route logic +} -PassThru | + Remove-PodeOAResponse -StatusCode 200 +``` + +### Requests + +#### Parameters + +You can set route parameter definitions, such as parameters passed in the path/query, by using the [`Set-PodeOARequest`](../../../Functions/OpenApi/Set-PodeOARequest) function with the `-Parameters` parameter. The parameter takes an array of properties converted into parameters, using the [`ConvertTo-PodeOAParameter`](../../../Functions/OpenApi/ConvertTo-PodeOAParameter) function. + +For example, to create some integer `userId` parameter that is supplied in the path of the request, the following will work: + +```powershell +Add-PodeRoute -Method Get -Path '/api/users/:userId' -ScriptBlock { + Write-PodeJsonResponse -Value @{ + Name = 'Rick' + UserId = $WebEvent.Parameters['userId'] + } +} -PassThru | + Set-PodeOARequest -Parameters @( + (New-PodeOAIntProperty -Name 'userId' -Required | ConvertTo-PodeOAParameter -In Path) + ) +``` + +Whereas you could use the next example to define 2 query parameters, both strings: + +```powershell +Add-PodeRoute -Method Get -Path '/api/users' -ScriptBlock { + Write-PodeJsonResponse -Value @{ + Name = 'Rick' + UserId = $WebEvent.Query['name'] + } +} -PassThru | + Set-PodeOARequest -Parameters ( + (New-PodeOAStringProperty -Name 'name' -Required | ConvertTo-PodeOAParameter -In Query), + (New-PodeOAStringProperty -Name 'city' -Required | ConvertTo-PodeOAParameter -In Query) + ) +``` + +#### Payload + +You can set request payload schemas by using the [`Set-PodeOARequest`](../../../Functions/OpenApi/Set-PodeOARequest)function, with the `-RequestBody` parameter. The request body can be defined using the [`New-PodeOARequestBody`](../../../Functions/OpenApi/New-PodeOARequestBody) function, and supplying schema definitions for content types - this works in very much a similar way to defining responses above. + +For example, to define a request JSON payload of some `userId` and `name` you could use the following: + +```powershell +Add-PodeRoute -Method Patch -Path '/api/users' -ScriptBlock { + Write-PodeJsonResponse -Value @{ + Name = $WebEvent.Data.name + UserId = $WebEvent.Data.userId + } +} -PassThru | + Set-PodeOARequest -RequestBody ( + New-PodeOARequestBody -Required -Content ( + New-PodeOAContentMediaType -ContentType 'application/json','application/xml' -Content ( New-PodeOAStringProperty -Name 'Name'| New-PodeOAIntProperty -Name 'UserId'| New-PodeOAObjectProperty ) ) + + ) +``` + +The expected payload would look as follows: + +```json +{ + "name": [string], + "userId": [integer] +} +``` + +```xml + + + + + +``` + diff --git a/docs/Tutorials/OpenAPI/2Properties.md b/docs/Tutorials/OpenAPI/2Properties.md new file mode 100644 index 000000000..207c94966 --- /dev/null +++ b/docs/Tutorials/OpenAPI/2Properties.md @@ -0,0 +1,297 @@ + +# Properties + +Properties are used to create all Parameters and Schemas in OpenAPI. You can use the simple types on their own, or you can combine multiple of them together to form complex objects. + +### Simple Types + +There are 5 simple property types: Integers, Numbers, Strings, Booleans, and Schemas. Each of which can be created using the following functions: + +* [`New-PodeOAIntProperty`](../../../Functions/OAProperties/New-PodeOAIntProperty) +* [`New-PodeOANumberProperty`](../../../Functions/OAProperties/New-PodeOANumberProperty) +* [`New-PodeOAStringProperty`](../../../Functions/OAProperties/New-PodeOAStringProperty) +* [`New-PodeOABoolProperty`](../../../Functions/OAProperties/New-PodeOABoolProperty) +* [`New-PodeOASchemaProperty`](../../../Functions//New-PodeOASchemaProperty) +* [`New-PodeOAMultiTypeProperty`](../../../Functions/OAProperties/New-PodeOAMultiTypeProperty) (Note: OpenAPI 3.1 only) + +These properties can be created with a Name, and other flags such as Required and/or a Description: + +```powershell +# simple integer +New-PodeOAIntProperty -Name 'userId' + +# a float number with a max value of 100 +New-PodeOANumberProperty -Name 'ratio' -Format Float -Maximum 100 + +# a string with a default value, and enum of options +New-PodeOAStringProperty -Name 'type' -Default 'admin' -Enum @('admin', 'user') + +# a boolean that's required +New-PodeOABoolProperty -Name 'enabled' -Required + +# a schema property that references another component schema +New-PodeOASchemaProperty -Name 'Config' -Reference 'ConfigSchema' + +# a string or an integer or a null value (only available with OpenAPI 3.1) +New-PodeOAMultiTypeProperty -Name 'multi' -Type integer,string -Nullable +``` + +On their own, like above, the simple properties don't really do much. However, you can combine that together to make complex objects/arrays as defined below. + +### Arrays + +There isn't a dedicated function to create an array property, instead there is an `-Array` switch on each of the property functions - both Object and the above simple properties. + +If you supply the `-Array` switch to any of the above simple properties, this will define an array of that type - the `-Name` parameter can also be omitted if only a simple array if required. + +For example, the below will define an integer array: + +```powershell +New-PodeOAIntProperty -Array +``` + +When used in a Response, this could return the following JSON example: + +```json +[ + 0, + 1, + 2 +] +``` + +### Objects + +An object property is a combination of multiple other properties - both simple, array of more objects. + +There are two ways to define objects: + +1. Similar to arrays, you can use the `-Object` switch on the simple properties. +2. You can use the [`New-PodeOAObjectProperty`](../../../Functions/OAProperties/New-PodeOAObjectProperty) function to combine multiple properties. + +#### Simple + +If you use the `-Object` switch on the simple property function, this will automatically wrap the property as an object. The Name for this is required. + +For example, the below will define a simple `userId` integer object: + +```powershell +New-PodeOAIntProperty -Name 'userId' -Object +``` + +In a response as JSON, this could look as follows: + +```json +{ + "userId": 0 +} +``` + +Furthermore, you can also supply both `-Array` and `-Object` switches: + +```powershell +New-PodeOAIntProperty -Name 'userId' -Object -Array +``` + +This wil result in something like the following JSON: + +```json +{ + "userId": [ 0, 1, 2 ] +} +``` + +#### Complex + +Unlike the `-Object` switch that simply converts a single property into an object, the [`New-PodeOAObjectProperty`](../../../Functions/OAProperties/New-PodeOAObjectProperty) function can combine and convert multiple properties. + +For example, the following will create an object using an Integer, String, and a Boolean: + +Legacy Definition + +```powershell +New-PodeOAObjectProperty -Properties ( + (New-PodeOAIntProperty -Name 'userId'), + (New-PodeOAStringProperty -Name 'name'), + (New-PodeOABoolProperty -Name 'enabled') +) +``` + +Using piping (new in Pode 2.10) + +```powershell +New-PodeOAIntProperty -Name 'userId'| New-PodeOAStringProperty -Name 'name'| + New-PodeOABoolProperty -Name 'enabled' |New-PodeOAObjectProperty +``` + +As JSON, this could look as follows: + +```json +{ + "userId": 0, + "name": "Gary Goodspeed", + "enabled": true +} +``` + +You can also supply the `-Array` switch to the [`New-PodeOAObjectProperty`](../../../Functions/OAProperties/New-PodeOAObjectProperty) function. This will result in an array of objects. For example, if we took the above: + +```powershell +New-PodeOAIntProperty -Name 'userId'| New-PodeOAStringProperty -Name 'name'| + New-PodeOABoolProperty -Name 'enabled' |New-PodeOAObjectProperty -Array +``` + +As JSON, this could look as follows: + +```json +[ + { + "userId": 0, + "name": "Gary Goodspeed", + "enabled": true + }, + { + "userId": 1, + "name": "Kevin", + "enabled": false + } +] +``` + +You can also combine objects into other objects: + +```powershell +$usersArray = New-PodeOAIntProperty -Name 'userId'| New-PodeOAStringProperty -Name 'name'| + New-PodeOABoolProperty -Name 'enabled' |New-PodeOAObjectProperty -Array + +New-PodeOAObjectProperty -Properties @( + (New-PodeOAIntProperty -Name 'found'), + $usersArray +) +``` + +As JSON, this could look as follows: + +```json +{ + "found": 2, + "users": [ + { + "userId": 0, + "name": "Gary Goodspeed", + "enabled": true + }, + { + "userId": 1, + "name": "Kevin", + "enabled": false + } + ] +} +``` + +### oneOf, anyOf and allOf Keywords + +OpenAPI 3.x provides several keywords which you can use to combine schemas. You can use these keywords to create a complex schema or validate a value against multiple criteria. + +* oneOf - validates the value against exactly one of the sub-schemas +* allOf - validates the value against all the sub-schemas +* anyOf - validates the value against any (one or more) of the sub-schemas + +You can use the [`Merge-PodeOAProperty`](../../../Functions/OAProperties/Merge-PodeOAProperty) will instead define a relationship between the properties. + +Unlike [`New-PodeOAObjectProperty`](../../../Functions/OAProperties/New-PodeOAObjectProperty) which combines and converts multiple properties into an Object, [`Merge-PodeOAProperty`](../../../Functions/OAProperties/Merge-PodeOAProperty) will instead define a relationship between the properties. + +For example, the following will create an something like an C Union object using an Integer, String, and a Boolean: + +```powershell +Merge-PodeOAProperty -Type OneOf -ObjectDefinitions @( + (New-PodeOAIntProperty -Name 'userId' -Object), + (New-PodeOAStringProperty -Name 'name' -Object), + (New-PodeOABoolProperty -Name 'enabled' -Object) + ) +``` + +Or + +```powershell +New-PodeOAIntProperty -Name 'userId' -Object | + New-PodeOAStringProperty -Name 'name' -Object | + New-PodeOABoolProperty -Name 'enabled' -Object | + Merge-PodeOAProperty -Type OneOf +``` + +As JSON, this could look as follows: + +```json +{ + "oneOf": [ + { + "type": "object", + "properties": { + "userId": { + "type": "integer" + } + } + }, + { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, + { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + } + } + } + ] +} +``` + +You can also supply a Component Schema created using [`Add-PodeOAComponentSchema`](../../../Functions/OAComponents/Add-PodeOAComponentSchema). For example, if we took the above: + +```powershell + New-PodeOAIntProperty -Name 'id'-Format Int64 -Example 1 -ReadOnly | + New-PodeOAStringProperty -Name 'username' -Example 'theUser' -Required | + New-PodeOAStringProperty -Name 'firstName' -Example 'John' | + New-PodeOAStringProperty -Name 'lastName' -Example 'James' | + New-PodeOAStringProperty -Name 'email' -Format email -Example 'john@email.com' | + New-PodeOAStringProperty -Name 'lastName' -Example 'James' | + New-PodeOAStringProperty -Name 'password' -Format Password -Example '12345' -Required | + New-PodeOAStringProperty -Name 'phone' -Example '12345' | + New-PodeOAIntProperty -Name 'userStatus'-Format int32 -Description 'User Status' -Example 1| + New-PodeOAObjectProperty -Name 'User' -XmlName 'user' | + Add-PodeOAComponentSchema + + New-PodeOAStringProperty -Name 'street' -Example '437 Lytton' -Required | + New-PodeOAStringProperty -Name 'city' -Example 'Palo Alto' -Required | + New-PodeOAStringProperty -Name 'state' -Example 'CA' -Required | + New-PodeOAStringProperty -Name 'zip' -Example '94031' -Required | + New-PodeOAObjectProperty -Name 'Address' -XmlName 'address' -Description 'Shipping Address' | + Add-PodeOAComponentSchema + + Merge-PodeOAProperty -Type AllOf -ObjectDefinitions 'Address','User' + +``` + +As JSON, this could look as follows: + +```json +{ + "allOf": [ + { + "$ref": "#/components/schemas/Address" + }, + { + "$ref": "#/components/schemas/User" + } + ] +} +``` \ No newline at end of file diff --git a/docs/Tutorials/OpenAPI/3Components.md b/docs/Tutorials/OpenAPI/3Components.md new file mode 100644 index 000000000..2610eb604 --- /dev/null +++ b/docs/Tutorials/OpenAPI/3Components.md @@ -0,0 +1,231 @@ +# Components + +You can define reusable OpenAPI components in Pode. Currently supported are Schemas, Parameters, Request Bodies, and Responses. + +### Schemas + +To define a reusable schema that can be used in request bodies, and responses, you can use the [`Add-PodeOAComponentSchema`](../../../Functions/OAComponents/Add-PodeOAComponentSchema) function. You'll need to supply a Name, and a Schema that can be reused. + +The following is an example of defining a schema which is a object of Name, UserId, and Age: + +```powershell +# define a reusable schema user object +New-PodeOAStringProperty -Name 'Name' | + New-PodeOAIntProperty -Name 'UserId' | + New-PodeOAIntProperty -Name 'Age' | + New-PodeOAObjectProperty | + Add-PodeOAComponentSchema -Name 'UserSchema' + +# reuse the above schema in a response +Add-PodeRoute -Method Get -Path '/api/users/:userId' -ScriptBlock { + Write-PodeJsonResponse -Value @{ + Name = 'Rick' + UserId = $WebEvent.Parameters['userId'] + Age = 42 + } +} -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'A list of users' -Content @{ + 'application/json' = 'UserSchema' + } +``` + +### Request Bodies + +To define a reusable request bodies you can use the [`Add-PodeOAComponentRequestBody`](../../../Functions/OAComponents/Add-PodeOAComponentRequestBody) function. You'll need to supply a Name, as well as the needed schemas for each content type. + +The following is an example of defining a JSON object that a Name, UserId, and an Enable flag: + +```powershell +# define a reusable request body +New-PodeOAContentMediaType -ContentType 'application/json', 'application/x-www-form-urlencoded' -Content ( + New-PodeOAStringProperty -Name 'Name' | + New-PodeOAIntProperty -Name 'UserId' | + New-PodeOABoolProperty -Name 'Enabled' | + New-PodeOAObjectProperty + ) | Add-PodeOAComponentRequestBody -Name 'UserBody' -Required + +# use the request body in a route +Add-PodeRoute -Method Patch -Path '/api/users' -ScriptBlock { + Set-PodeResponseStatus -StatusCode 200 +} -PassThru | + Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Reference 'UserBody') +``` + +The JSON payload expected is of the format: + +```json +{ + "Name": [string], + "UserId": [integer], + "Enabled": [boolean] +} +``` + +### Parameters + +To define reusable parameters that are used on requests, you can use the [`Add-PodeOAComponentParameter`](../../../Functions/OAComponents/Add-PodeOAComponentParameter) function. You'll need to supply a Name and the Parameter definition. + +The following is an example of defining an integer path parameter for a `userId`, and then using that parameter on a route. + +```powershell +# define a reusable {userid} path parameter +New-PodeOAIntProperty -Name 'userId' -Required | ConvertTo-PodeOAParameter -In Path |Add-PodeOAComponentParameter -Name 'UserId' + +# use this parameter in a route +Add-PodeRoute -Method Get -Path '/api/users/:userId' -ScriptBlock { + Write-PodeJsonResponse -Value @{ + Name = 'Rick' + UserId = $WebEvent.Parameters['userId'] + } +} -PassThru | + Set-PodeOARequest -Parameters @(ConvertTo-PodeOAParameter -Reference 'UserId') +``` + +### Responses + +To define a reusable response definition you can use the [`Add-PodeOAComponentResponse`](../../../Functions/OAComponents/Add-PodeOAComponentResponse) function. You'll need to supply a Name, and optionally any Content/Header schemas that define the responses payload. + +The following is an example of defining a 200 response with a JSON payload of an array of objects for Name/UserId. The Response component can be used by a route referencing the name: + +```powershell +# defines a response with a json payload using New-PodeOAContentMediaType +Add-PodeOAComponentResponse -Name 'OK' -Description 'A user object' -Content ( + New-PodeOAContentMediaType -ContentType 'application/json' -Array -Content ( + New-PodeOAStringProperty -Name 'Name' | + New-PodeOAIntProperty -Name 'UserId' | + New-PodeOAObjectProperty + ) + ) + +# reuses the above response on a route using its "OK" name +Add-PodeRoute -Method Get -Path "/api/users" -ScriptBlock { + Write-PodeJsonResponse -Value @( + @{ Name = 'Rick'; UserId = 123 }, + @{ Name = 'Geralt'; UserId = 124 } + ) +} -PassThru | + Add-PodeOAResponse -StatusCode 200 -Reference 'OK' +``` + +the JSON response payload defined is as follows: + +```json +[ + { + "Name": [string], + "UserId": [integer] + } +] +``` + + +### Examples + +To define a reusable example definition you can use the [`Add-PodeOAComponentExample`](../../../Functions/OAComponents/Add-PodeOAComponentExample) function. You'll need to supply a Name, a Summary and a list of value representing the object. + +The following is an example that defines three Pet examples request bodies, and how they're used in a Route's OpenAPI definition: + +```powershell + # defines the frog example +Add-PodeOAComponentExample -name 'frog-example' -Summary "An example of a frog with a cat's name" -Value @{ + name = 'Jaguar'; petType = 'Panthera'; color = 'Lion'; gender = 'Male'; breed = 'Mantella Baroni' +} +# defines the cat example +Add-PodeOAComponentExample -Name 'cat-example' -Summary 'An example of a cat' -Value @{ + name = 'Fluffy'; petType = 'Cat'; color = 'White'; gender = 'male'; breed = 'Persian' +} +# defines the dog example +Add-PodeOAComponentExample -Name 'dog-example' -Summary "An example of a dog with a cat's name" -Value @{ + name = 'Puma'; petType = 'Dog'; color = 'Black'; gender = 'Female'; breed = 'Mixed' +} + +# reuses the examples +Add-PodeRoute -PassThru -Method Put -Path '/pet/:petId' -ScriptBlock { + # route code +} | Set-PodeOARouteInfo -Summary 'Updates a pet in the store with form data' -Tags 'pet' ` + -OperationId 'updatepet' -PassThru | + Set-PodeOARequest -Parameters @( + (New-PodeOAStringProperty -Name 'petId' -Description 'ID of pet that needs to be updated' | ConvertTo-PodeOAParameter -In Path -Required) + ) -RequestBody ( + New-PodeOARequestBody -Description 'user to add to the system' -Content @{ 'application/json' = 'Pet' } -Examples ( + New-PodeOAExample -ContentType 'application/json', 'application/xml' -Reference 'cat-example' | + New-PodeOAExample -ContentType 'application/json', 'application/xml' -Reference 'dog-example' | + New-PodeOAExample -ContentType 'application/json', 'application/xml' -Reference 'frog-example' + ) + ) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Pet updated.' +``` + +### Headers + +To define a reusable header definition you can use the [`Add-PodeOAComponentHeader`](../../../Functions/OAComponents/Add-PodeOAComponentHeader) function. You'll need to supply a Name, and optionally any Content/Header schemas that define the responses payload. + +```powershell + # define Headers +New-PodeOAIntProperty -Format Int32 -Description 'calls per hour allowed by the user' | + Add-PodeOAComponentHeader -Name 'X-Rate-Limit' +New-PodeOAStringProperty -Format Date-Time -Description 'date in UTC when token expires' | + Add-PodeOAComponentHeader -Name 'X-Expires-After' + +Add-PodeRoute -PassThru -Method Get -Path '/user/login' -ScriptBlock { + # route code +} | Set-PodeOARouteInfo -Summary 'Logs user into the system.' -Description 'Logs user into the system.' ` + -Tags 'user' -OperationId 'loginUser' -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' ` + -Header @('X-Rate-Limit', 'X-Expires-After') -Content ( + New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'string' + ) -PassThru | + Add-PodeOAResponse -StatusCode 400 -Description 'Invalid username/password supplied' +``` + + +### CallBacks + +To define a reusable callback definition you can use the [`Add-PodeOAComponentCallBack`](../../../Functions/OAComponents/Add-PodeOAComponentCallBack) function. You'll need to supply a Name, and optionally any Content/Header schemas that define the responses payload. + +```powershell +Add-PodeRoute -PassThru -Method Post -Path '/petcallbackReference' -Authentication 'Login-OAuth2' ` + -Scope 'write' -ScriptBlock { + #route code +} | Set-PodeOARouteInfo -Summary 'Add a new pet to the store' -Description 'Add a new pet to the store' ` + -Tags 'pet' -OperationId 'petcallbackReference' -PassThru | + Set-PodeOARequest -RequestBody ( New-PodeOARequestBody -Reference 'PetBodySchema' ) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content ( + New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet' + ) -PassThru | + Add-PodeOAResponse -StatusCode 405 -Description 'Validation exception' -Content @{ + 'application / json' = ( New-PodeOAStringProperty -Name 'result' | + New-PodeOAStringProperty -Name 'message' | + New-PodeOAObjectProperty ) + } -PassThru | + Add-PodeOACallBack -Name 'test1' -Reference 'test' +``` + +### Response Links + +To define a reusable response link definition you can use the [`Add-PodeOAComponentResponseLink`](../../../Functions/OAComponents/Add-PodeOAComponentResponseLink) function. You'll need to supply a Name, and optionally any Content/Header schemas that define the responses payload. + +```powershell +#Add link reference +Add-PodeOAComponentResponseLink -Name 'address' -OperationId 'getUserByName' -Parameters @{ + 'username' = '$request.path.username' +} + +#use link reference +Add-PodeRoute -PassThru -Method Put -Path '/userLinkByRef/:username' -ScriptBlock { + Write-PodeJsonResponse -Value 'done' -StatusCode 200 +} | Set-PodeOARouteInfo -Summary 'Update user' -Description 'This can only be done by the logged in user.' ` + -Tags 'user' -OperationId 'updateUserLinkByRef' -PassThru | + Set-PodeOARequest -Parameters ( + ( New-PodeOAStringProperty -Name 'username' -Description ' name that need to be updated.' -Required | ConvertTo-PodeOAParameter -In Path ) + ) -RequestBody ( + New-PodeOARequestBody -Required -Content ( + New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'User' ) + ) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Content @{'application/json' = 'User' } -PassThru -Links ( + New-PodeOAResponseLink -Name 'address2' -Reference 'address' + ) | + Add-PodeOAResponse -StatusCode 400 -Description 'Invalid username supplied' -PassThru | + Add-PodeOAResponse -StatusCode 404 -Description 'User not found' -PassThru | + Add-PodeOAResponse -StatusCode 405 -Description 'Invalid Input' +``` \ No newline at end of file diff --git a/docs/Tutorials/OpenAPI/4DocumentationTools.md b/docs/Tutorials/OpenAPI/4DocumentationTools.md new file mode 100644 index 000000000..34b6822b7 --- /dev/null +++ b/docs/Tutorials/OpenAPI/4DocumentationTools.md @@ -0,0 +1,41 @@ + +# Documentation Tools + +If you're not using a custom OpenAPI viewer, then you can use one or more of the inbuilt which Pode supports: ones with Pode: + +* Swagger +* ReDoc +* RapiDoc +* StopLight +* Explorer +* RapiPdf + +For each you can customise the Route path to access the page on, but by default Swagger is at `/swagger`, ReDoc is at `/redoc`, etc. If you've written your own custom OpenAPI definition then you can also set a custom Route path to fetch the definition on. + +To enable a viewer you can use the [`Enable-PodeOAViewer`](../../../Functions/OpenApi/Enable-PodeOAViewer) function: + +```powershell +# for swagger at "/docs/swagger" +Enable-PodeOAViewer -Type Swagger -Path '/docs/swagger' -DarkMode + +# and ReDoc at the default "/redoc" +Enable-PodeOAViewer -Type ReDoc -Path '/docs/redoc' + +# and RapiDoc at "/docs/rapidoc" +Enable-PodeOAViewer -Type RapiDoc -Path '/docs/rapidoc' + +# and StopLight at "/docs/stoplight" +Enable-PodeOAViewer -Type StopLight -Path '/docs/stoplight' + +# and Explorer at "/docs/explorer" +Enable-PodeOAViewer -Type Explorer -Path '/docs/explorer' + +# and RapiPdf at "/docs/rapipdf" +Enable-PodeOAViewer -Type RapiPdf -Path '/docs/rapipdf' + +# plus a bookmark page with the link to all documentation +Enable-PodeOAViewer -Bookmarks -Path '/docs' + +# there is also an OpenAPI editor (only for v3.0.x) +Enable-PodeOAViewer -Editor -Path '/docs/swagger-editor' +``` diff --git a/docs/Tutorials/OpenAPI/5ParameterValidation.md b/docs/Tutorials/OpenAPI/5ParameterValidation.md new file mode 100644 index 000000000..e1693997a --- /dev/null +++ b/docs/Tutorials/OpenAPI/5ParameterValidation.md @@ -0,0 +1,50 @@ + +## Parameter Validation + +Is possible to validate any parameter submitted by clients against an OpenAPI schema, ensuring adherence to defined standards. + + +First, schema validation has to be enabled using : + +```powershell +Enable-PodeOpenApi -EnableSchemaValidation #any other parameters needed +``` + +This command activates the OpenAPI feature with schema validation enabled, ensuring strict adherence to specified schemas. + +Next, is possible to validate any route using `Test-PodeOAJsonSchemaCompliance`. +In this example, we'll create a route for updating a pet: + +```powershell +Add-PodeRoute -PassThru -Method Post -Path '/user' -ScriptBlock { + $contentType = Get-PodeHeader -Name 'Content-Type' + $responseMediaType = Get-PodeHeader -Name 'Accept' + switch ($contentType) { + 'application/xml' { + $user = ConvertFrom-PodeXml -node $WebEvent.data | ConvertTo-Json + } + 'application/json' { $user = ConvertTo-Json $WebEvent.data } + 'application/x-www-form-urlencoded' { $user = ConvertTo- Json $WebEvent.data } + default { + Write-PodeHtmlResponse -StatusCode 415 + return + } + } + $Validate = Test-PodeOAJsonSchemaCompliance -Json $user -SchemaReference 'User' + if ($Validate.result) { + $newUser = Add-user -User (convertfrom-json -InputObject $user -AsHashtable) + Save-PodeState -Path $using:PetDataJson + switch ($responseMediaType) { + 'application/xml' { Write-PodeXmlResponse -Value $newUser -StatusCode 200 } + 'application/json' { Write-PodeJsonResponse -Value $newUser -StatusCode 200 } + default { Write-PodeHtmlResponse -StatusCode 415 } + } + } + else { + Write-PodeHtmlResponse -StatusCode 405 -Value ($Validate.message -join ', ') + } +} | Set-PodeOARouteInfo -Summary 'Create user.' -Description 'This can only be done by the logged in user.' -Tags 'user' -OperationId 'createUser' -PassThru | + Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'User' )) -PassThru | + Add-PodeOAResponse -StatusCode 405 -Description 'Invalid Input' -PassThru | + Add-PodeOAResponse -Default -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'User' ) +``` diff --git a/docs/Tutorials/OpenAPI/6MultipleDefinitions.md b/docs/Tutorials/OpenAPI/6MultipleDefinitions.md new file mode 100644 index 000000000..c3278723f --- /dev/null +++ b/docs/Tutorials/OpenAPI/6MultipleDefinitions.md @@ -0,0 +1,129 @@ + +# Multiple definitions + +It's possible to create multiple OpenAPI definitions inside the same Server instance. This feature could be useful in situations such as: + +* Multiple versions of the OpenAPI specification for different use cases +* The same OpenAPI definition, but one using OpenAPI v3.0.3 and another using v3.1.0 +* Different APIs based on the IP or URL + + +### How to use it +Any Pode function that interacts with OpenAPI has a `-DefinitionTag [string[]]` parameter available. This allows you to specify within which OpenAPI definition(s) the API's definition should be available. + +!!! note + These functions accept a simple string, and not an array + + * Get-PodeOADefinition + * Enable-PodeOpenApi + * Enable-PodeOAViewer + * Add-PodeOAInfo + * Test-PodeOAJsonSchemaCompliance + +A new OpenAPI definition has to be created using the `Enable-PodeOpenApi` function + +```powershell +Enable-PodeOpenApi -Path '/docs/openapi/v3.0' -OpenApiVersion '3.0.3' -DefinitionTag 'v3' +Enable-PodeOpenApi -Path '/docs/openapi/v3.1' -OpenApiVersion '3.1.0' -DefinitionTag 'v3.1' +Enable-PodeOpenApi -Path '/docs/openapi/admin' -OpenApiVersion '3.1.0' -DefinitionTag 'admin' +``` + +There is also [`Select-PodeOADefinition`](../../../Functions/OpenApi/Select-PodeOADefinition), which simplifies the selection of which OpenAPI definition to use as a wrapper around multiple OpenAPI functions, or Route functions. Meaning you don't have to specify `-DefinitionTag` on embedded OpenAPI/Route calls: + +```powershell +Select-PodeOADefinition -Tag 'v3', 'v3.1' -Scriptblock { + Add-PodeRouteGroup -Path '/api/v5' -Routes { + Add-PodeRoute -Method Get -Path '/petbyRef/:petId' -ScriptBlock { + Write-PodeJsonResponse -Value 'done' -StatusCode 2005 + } + } +} + +Select-PodeOADefinition -Tag 'admin' -ScriptBlock { + # your admin definition +} +``` + +The default `Definition Tag` is named "default". This can be changed using the `Server.psd1` file and the `Web.OpenApi.DefaultDefinitionTag` property + +```powershell +@{ + Web=@{ + OpenApi=@{ + DefaultDefinitionTag= 'NewDefault' + } + } +} +``` + +### Renaming a Definition Tag + +A Definition Tag can be renamed at any time using the `Rename-PodeOADefinitionTagName` function. This allows you to update the tag name for an existing OpenAPI definition, ensuring your tags remain organized and meaningful. + +```powershell +Rename-PodeOADefinitionTagName -Tag 'v.3' -NewTag 'v.3.0.3' +``` + +In this example, the tag `'v.3'` is renamed to `'v.3.0.3'`. + +### Renaming the Default Definition Tag + +You can also rename the default `Definition Tag` without specifying the `Tag` parameter. This updates the default tag to the new name provided. + +```powershell +Rename-PodeOADefinitionTagName -NewTag 'NewDefault' +``` + +In this example, the default definition tag is renamed to `'NewDefault'`. + +!!! note + The `Rename-PodeOADefinitionTagName` function cannot be used inside a `Select-PodeOADefinition` `[Scriptblock]`. Attempting to do so will result in an error. + +### OpenAPI example + +A simple OpenAPI definition + +```powershell +Add-PodeOAInfo -Title 'Swagger Petstore - OpenAPI 3.0' -Version 1.0.17 -Description $InfoDescription -TermsOfService 'http://swagger.io/terms/' -LicenseName 'Apache 2.0' ` + -LicenseUrl 'http://www.apache.org/licenses/LICENSE-2.0.html' -ContactName 'API Support' -ContactEmail 'apiteam@swagger.io' -DefinitionTag 'v3' + +Add-PodeOAInfo -Title 'Swagger Petstore - OpenAPI 3.1' -Version 1.0.17 -Description $InfoDescription -TermsOfService 'http://swagger.io/terms/' -LicenseName 'Apache 2.0' ` + -LicenseUrl 'http://www.apache.org/licenses/LICENSE-2.0.html' -ContactName 'API Support' -ContactEmail 'apiteam@swagger.io' -DefinitionTag 'v3.1' + +Add-PodeOAServerEndpoint -url '/api/v3' -Description 'default endpoint' -DefinitionTag 'v3', 'v3.1' + +#OpenAPI 3.0 +Enable-PodeOAViewer -Type Swagger -Path '/docs/swagger' -DefinitionTag 'v3' +Enable-PodeOAViewer -Type Bookmarks -Path '/docs' -DefinitionTag 'v3' + +#OpenAPI 3.1 +Enable-PodeOAViewer -Type Swagger -Path '/docs/v3.1/swagger' -DefinitionTag 'v3.1' +Enable-PodeOAViewer -Type ReDoc -Path '/docs/v3.1/redoc' -DarkMode -DefinitionTag 'v3.1' +Enable-PodeOAViewer -Type Bookmarks -Path '/docs/v3.1' -DefinitionTag 'v3.1' + +Select-PodeOADefinition -Tag 'v3', 'v3.1' -ScriptBlock { + New-PodeOAIntProperty -Name 'id'-Format Int64 -Example 10 -Required | + New-PodeOAStringProperty -Name 'name' -Example 'doggie' -Required | + New-PodeOASchemaProperty -Name 'category' -Reference 'Category' | + New-PodeOAStringProperty -Name 'photoUrls' -Array -XmlWrapped -XmlItemName 'photoUrl' -Required | + New-PodeOASchemaProperty -Name 'tags' -Reference 'Tag' -Array -XmlWrapped | + New-PodeOAStringProperty -Name 'status' -Description 'pet status in the store' -Enum @('available', 'pending', 'sold') | + New-PodeOAObjectProperty -XmlName 'pet' | + Add-PodeOAComponentSchema -Name 'Pet' + + + Add-PodeRouteGroup -Path '/api/v3' -Routes { + Add-PodeRoute -PassThru -Method Put -Path '/pet' -Authentication 'merged_auth_nokey' -Scope 'write:pets', 'read:pets' -ScriptBlock { + #code + } | Set-PodeOARouteInfo -Summary 'Update an existing pet' -Description 'Update an existing pet by Id' -Tags 'pet' -OperationId 'updatePet' -PassThru | + Set-PodeOARequest -RequestBody ( + New-PodeOARequestBody -Description 'Update an existent pet in the store' -Required -Content ( + New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet' ) + ) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet' ) -PassThru | + Add-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' -PassThru | + Add-PodeOAResponse -StatusCode 404 -Description 'Pet not found' -PassThru | + Add-PodeOAResponse -StatusCode 405 -Description 'Validation exception' + } +} +``` diff --git a/docs/Tutorials/OpenAPI/Overview.md b/docs/Tutorials/OpenAPI/Overview.md deleted file mode 100644 index 66098b2e5..000000000 --- a/docs/Tutorials/OpenAPI/Overview.md +++ /dev/null @@ -1,1043 +0,0 @@ -# Overview - -Pode has built-in support for converting your routes into OpenAPI 3.0 definitions. There is also support for enabling simple Swagger and/or ReDoc viewers and others. - -The OpenApi module has been extended with many more functions, and some old ones have been improved. - -For more detailed information regarding OpenAPI and Pode, please refer to [OpenAPI Specification and Pode](../Specification/v3_0_3.md) - -You can enable OpenAPI in Pode, and a straightforward definition will be generated. However, to get a more complex definition with request bodies, parameters, and response payloads, you'll need to use the relevant OpenAPI functions detailed below. - -## Enabling OpenAPI - -To enable support for generating OpenAPI definitions you'll need to use the [`Enable-PodeOpenApi`](../../../Functions/OpenApi/Enable-PodeOpenApi) function. This will allow you to set a title and version for your API. You can also set a default route to retrieve the OpenAPI definition for tools like Swagger or ReDoc, the default is at `/openapi`. - -You can also set a route filter (such as `/api/*`, the default is `/*` for everything), so only those routes are included in the definition. - -An example of enabling OpenAPI is a follows: - -```powershell -Enable-PodeOpenApi -Title 'My Awesome API' -Version 9.0.0.1 -``` - -An example of setting the OpenAPI route is a follows. This will create a route accessible at `/docs/openapi`: - -```powershell -Enable-PodeOpenApi -Path '/docs/openapi' -Title 'My Awesome API' -Version 9.0.0.1 -``` - -### Default Setup - -In the very simplest of scenarios, just enabling OpenAPI will generate a minimal definition. It can be viewed in Swagger/ReDoc etc, but won't be usable for trying calls. - -When you enable OpenAPI, and don't set any other OpenAPI data, the following is the minimal data that is included: - -* Every route will have a 200 and Default response -* Although routes will be included, no request bodies, parameters or response payloads will be defined -* If you have multiple endpoints, then the servers section will be included -* Any authentication will be included - -This can be changed with [`Enable-PodeOpenApi`](../../../Functions/OpenApi/Enable-PodeOpenApi) - -For example to change the default response 404 and 500 - -```powershell -Enable-PodeOpenApi -Path '/docs/openapi' -OpenApiVersion '3.0.3' -DefaultResponses ( - New-PodeOAResponse -StatusCode 404 -Description 'User not found' | Add-PodeOAResponse -StatusCode 500 - ) -``` - -For disabling the Default Response use: - -```powershell -Enable-PodeOpenApi -Path '/docs/openapi' -OpenApiVersion '3.0.3' -NoDefaultResponses -``` - -For disabling the Minimal Definitions feature use: - -```powershell -Enable-PodeOpenApi -Path '/docs/openapi' -OpenApiVersion '3.0.3' -DisableMinimalDefinitions -``` - -### Get Definition - -Instead of defining a route to return the definition, you can write the definition to the response whenever you want, and in any route, using the [`Get-PodeOADefinition`](../../../Functions/OpenApi/Get-PodeOADefinition) function. This could be useful in certain scenarios like in Azure Functions, where you can enable OpenAPI, and then write the definition to the response of a GET request if some query parameter is set; eg: `?openapi=1`. - -For example: - -```powershell -Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - if ($WebEvent.Query.openapi -eq 1) { - Get-PodeOpenApiDefinition | Write-PodeJsonResponse - } -} -``` - -## OpenAPI Info object - -In previous releases some of the Info object properties like Version and Title were defined by [`Enable-PodeOpenApi`](../../../Functions/OpenApi/Enable-PodeOpenApi). -Starting from version 2.10 a new [`Add-PodeOAInfo`](../../../Functions/OpenApi/Add-PodeOAInfo) function has been added to create a full OpenAPI Info spec. - -```powershell -Add-PodeOAInfo -Title 'Swagger Petstore - OpenAPI 3.0' ` - -Version 1.0.17 ` - -Description $InfoDescription ` - -TermsOfService 'http://swagger.io/terms/' ` - -LicenseName 'Apache 2.0' ` - -LicenseUrl 'http://www.apache.org/licenses/LICENSE-2.0.html' ` - -ContactName 'API Support' ` - -ContactEmail 'apiteam@swagger.io' -``` - -## OpenAPI configuration Best Practice - -Pode is rich of functions to create and configure an complete OpenApi spec. Here is a typical code you should use to initiate an OpenApi spec - -```powershell -#Initialize OpenApi -Enable-PodeOpenApi -Path '/docs/openapi' -Title 'Swagger Petstore - OpenAPI 3.0' ` - -OpenApiVersion 3.1 -DisableMinimalDefinitions -NoDefaultResponses - -# OpenApi Info -Add-PodeOAInfo -Title 'Swagger Petstore - OpenAPI 3.0' ` - -Version 1.0.17 ` - -Description 'This is a sample Pet Store Server based on the OpenAPI 3.0 specification. ...' ` - -TermsOfService 'http://swagger.io/terms/' ` - -LicenseName 'Apache 2.0' ` - -LicenseUrl 'http://www.apache.org/licenses/LICENSE-2.0.html' ` - -ContactName 'API Support' ` - -ContactEmail 'apiteam@swagger.io' ` - -ContactUrl 'http://example.com/support' - -# Endpoint for the API - Add-PodeOAServerEndpoint -url '/api/v3.1' -Description 'default endpoint' - - # OpenApi external documentation links - $extDoc = New-PodeOAExternalDoc -Name 'SwaggerDocs' -Description 'Find out more about Swagger' -Url 'http://swagger.io' - $extDoc | Add-PodeOAExternalDoc - - # OpenApi documentation viewer - Enable-PodeOAViewer -Type Swagger -Path '/docs/swagger' - Enable-PodeOAViewer -Type ReDoc -Path '/docs/redoc' - Enable-PodeOAViewer -Type RapiDoc -Path '/docs/rapidoc' - Enable-PodeOAViewer -Type StopLight -Path '/docs/stoplight' - Enable-PodeOAViewer -Type Explorer -Path '/docs/explorer' - Enable-PodeOAViewer -Bookmarks -Path '/docs' -``` - -## Authentication - -Any authentication defined, either by [`Add-PodeAuthMiddleware`](../../../Functions/Authentication/Add-PodeAuthMiddleware), or using the `-Authentication` parameter on Routes, will be automatically added to the `security` section of the OpenAPI definition. - - -## Tags - -In OpenAPI, a "tag" is used to group related operations. Tags are often used to organize and categorize endpoints in an API specification, making it easier to understand and navigate the API documentation. Each tag can be associated with one or more API operations, and these tags are then used in tools like Swagger UI to group and display operations in a more organized way. - -Here's an example of how to define and use tags: - -```powershell -# create an External Doc reference -$swaggerDocs = New-PodeOAExternalDoc -Description 'Find out more about Swagger' -Url 'http://swagger.io' - -# create a Tag -Add-PodeOATag -Name 'pet' -Description 'Everything about your Pets' -ExternalDoc $swaggerDocs - -Add-PodeRoute -PassThru -Method get -Path '/pet/findByStatus' -Authentication 'Login-OAuth2' -Scope 'read' -AllowAnon -ScriptBlock { - #route code -} | Set-PodeOARouteInfo -Summary 'Finds Pets by status' -Description 'Multiple status values can be provided with comma-separated strings' ` - -Tags 'pet' -OperationId 'findPetsByStatus' -``` - -## Routes - -To extend the definition of a route, you can use the `-PassThru` switch on the [`Add-PodeRoute`](../../../Functions/Routes/Add-PodeRoute) function. This will cause the route that was created to be returned, so you can pass it down the pipe into more OpenAPI functions. - -To add metadata to a route's definition you can use the [`Set-PodeOARouteInfo`](../../../Functions/OpenApi/Set-PodeOARouteInfo) function. This will allow you to define a summary/description for the route, as well as tags for grouping: - -```powershell -Add-PodeRoute -Method Get -Path "/api/resources" -ScriptBlock { - Set-PodeResponseStatus -Code 200 -} -PassThru | - Set-PodeOARouteInfo -Summary 'Retrieve some resources' -Tags 'Resources' -``` - -Each of the following OpenAPI functions have a `-PassThru` switch, allowing you to chain many of them together. - -### Responses - -You can define multiple responses for a route, but only one of each status code, using the [`Add-PodeOAResponse`](../../../Functions/OpenApi/Add-PodeOAResponse) function. You can either just define the response and status code, with a custom description, or with a schema defining the payload of the response. - -The following is an example of defining simple 200 and 404 responses on a route: - -```powershell -Add-PodeRoute -Method Get -Path "/api/user/:userId" -ScriptBlock { - # logic -} -PassThru | - Add-PodeOAResponse -StatusCode 200 -PassThru | - Add-PodeOAResponse -StatusCode 404 -Description 'User not found' -``` - -Whereas the following is a more complex definition, which also defines the responses JSON payload. This payload is defined as an object with a string Name, and integer UserId: - -```powershell -Add-PodeRoute -Method Get -Path '/api/users/:userId' -ScriptBlock { - Write-PodeJsonResponse -Value @{ - Name = 'Rick' - UserId = $WebEvent.Parameters['userId'] - } -} -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'A user object' --Content @{ - 'application/json' = (New-PodeOAStringProperty -Name 'Name'| - New-PodeOAIntProperty -Name 'UserId'| New-PodeOAObjectProperty) - } -``` - -the JSON response payload defined is as follows: - -```json -{ - "Name": [string], - "UserId": [integer] -} -``` - -In case the response JSON payload is an array - -```powershell -Add-PodeRoute -Method Get -Path '/api/users/:userId' -ScriptBlock { - Write-PodeJsonResponse -Value @{ - Name = 'Rick' - UserId = $WebEvent.Parameters['userId'] - } - } -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'A user object' -Content ( - New-PodeOAContentMediaType -ContentMediaType 'application/json' -Array -Content ( - New-PodeOAStringProperty -Name 'Name' | - New-PodeOAIntProperty -Name 'UserId' | - New-PodeOAObjectProperty - ) - ) -``` - -```json -[ - { - "Name": [string], - "UserId": [integer] - } -] -``` - -Internally, each route is created with an empty default 200 and 500 response. You can remove these, or other added responses, by using [`Remove-PodeOAResponse`](../../../Functions/OpenApi/Remove-PodeOAResponse): - -```powershell -Add-PodeRoute -Method Get -Path "/api/user/:userId" -ScriptBlock { - # route logic -} -PassThru | - Remove-PodeOAResponse -StatusCode 200 -``` - -### Requests - -#### Parameters - -You can set route parameter definitions, such as parameters passed in the path/query, by using the [`Set-PodeOARequest`](../../../Functions/OpenApi/Set-PodeOARequest) function with the `-Parameters` parameter. The parameter takes an array of properties converted into parameters, using the [`ConvertTo-PodeOAParameter`](../../../Functions/OpenApi/ConvertTo-PodeOAParameter) function. - -For example, to create some integer `userId` parameter that is supplied in the path of the request, the following will work: - -```powershell -Add-PodeRoute -Method Get -Path '/api/users/:userId' -ScriptBlock { - Write-PodeJsonResponse -Value @{ - Name = 'Rick' - UserId = $WebEvent.Parameters['userId'] - } -} -PassThru | - Set-PodeOARequest -Parameters @( - (New-PodeOAIntProperty -Name 'userId' -Required | ConvertTo-PodeOAParameter -In Path) - ) -``` - -Whereas you could use the next example to define 2 query parameters, both strings: - -```powershell -Add-PodeRoute -Method Get -Path '/api/users' -ScriptBlock { - Write-PodeJsonResponse -Value @{ - Name = 'Rick' - UserId = $WebEvent.Query['name'] - } -} -PassThru | - Set-PodeOARequest -Parameters ( - (New-PodeOAStringProperty -Name 'name' -Required | ConvertTo-PodeOAParameter -In Query), - (New-PodeOAStringProperty -Name 'city' -Required | ConvertTo-PodeOAParameter -In Query) - ) -``` - -#### Payload - -You can set request payload schemas by using the [`Set-PodeOARequest`](../../../Functions/OpenApi/Set-PodeOARequest)function, with the `-RequestBody` parameter. The request body can be defined using the [`New-PodeOARequestBody`](../../../Functions/OpenApi/New-PodeOARequestBody) function, and supplying schema definitions for content types - this works in very much a similar way to defining responses above. - -For example, to define a request JSON payload of some `userId` and `name` you could use the following: - -```powershell -Add-PodeRoute -Method Patch -Path '/api/users' -ScriptBlock { - Write-PodeJsonResponse -Value @{ - Name = $WebEvent.Data.name - UserId = $WebEvent.Data.userId - } -} -PassThru | - Set-PodeOARequest -RequestBody ( - New-PodeOARequestBody -Required -Content ( - New-PodeOAContentMediaType -ContentMediaType 'application/json','application/xml' -Content ( New-PodeOAStringProperty -Name 'Name'| New-PodeOAIntProperty -Name 'UserId'| New-PodeOAObjectProperty ) ) - - ) -``` - -The expected payload would look as follows: - -```json -{ - "name": [string], - "userId": [integer] -} -``` - -```xml - - - - - -``` - -## Components - -You can define reusable OpenAPI components in Pode. Currently supported are Schemas, Parameters, Request Bodies, and Responses. - -### Schemas - -To define a reusable schema that can be used in request bodies, and responses, you can use the [`Add-PodeOAComponentSchema`](../../../Functions/OAComponents/Add-PodeOAComponentSchema) function. You'll need to supply a Name, and a Schema that can be reused. - -The following is an example of defining a schema which is a object of Name, UserId, and Age: - -```powershell -# define a reusable schema user object -New-PodeOAStringProperty -Name 'Name' | - New-PodeOAIntProperty -Name 'UserId' | - New-PodeOAIntProperty -Name 'Age' | - New-PodeOAObjectProperty | - Add-PodeOAComponentSchema -Name 'UserSchema' - -# reuse the above schema in a response -Add-PodeRoute -Method Get -Path '/api/users/:userId' -ScriptBlock { - Write-PodeJsonResponse -Value @{ - Name = 'Rick' - UserId = $WebEvent.Parameters['userId'] - Age = 42 - } -} -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'A list of users' -Content @{ - 'application/json' = 'UserSchema' - } -``` - -### Request Bodies - -To define a reusable request bodies you can use the [`Add-PodeOAComponentRequestBody`](../../../Functions/OAComponents/Add-PodeOAComponentRequestBody) function. You'll need to supply a Name, as well as the needed schemas for each content type. - -The following is an example of defining a JSON object that a Name, UserId, and an Enable flag: - -```powershell -# define a reusable request body -New-PodeOAContentMediaType -ContentMediaType 'application/json', 'application/x-www-form-urlencoded' -Content ( - New-PodeOAStringProperty -Name 'Name' | - New-PodeOAIntProperty -Name 'UserId' | - New-PodeOABoolProperty -Name 'Enabled' | - New-PodeOAObjectProperty - ) | Add-PodeOAComponentRequestBody -Name 'UserBody' -Required - -# use the request body in a route -Add-PodeRoute -Method Patch -Path '/api/users' -ScriptBlock { - Set-PodeResponseStatus -StatusCode 200 -} -PassThru | - Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Reference 'UserBody') -``` - -The JSON payload expected is of the format: - -```json -{ - "Name": [string], - "UserId": [integer], - "Enabled": [boolean] -} -``` - -### Parameters - -To define reusable parameters that are used on requests, you can use the [`Add-PodeOAComponentParameter`](../../../Functions/OAComponents/Add-PodeOAComponentParameter) function. You'll need to supply a Name and the Parameter definition. - -The following is an example of defining an integer path parameter for a `userId`, and then using that parameter on a route. - -```powershell -# define a reusable {userid} path parameter -New-PodeOAIntProperty -Name 'userId' -Required | ConvertTo-PodeOAParameter -In Path |Add-PodeOAComponentParameter -Name 'UserId' - -# use this parameter in a route -Add-PodeRoute -Method Get -Path '/api/users/:userId' -ScriptBlock { - Write-PodeJsonResponse -Value @{ - Name = 'Rick' - UserId = $WebEvent.Parameters['userId'] - } -} -PassThru | - Set-PodeOARequest -Parameters @(ConvertTo-PodeOAParameter -Reference 'UserId') -``` - -### Responses - -To define a reusable response definition you can use the [`Add-PodeOAComponentResponse`](../../../Functions/OAComponents/Add-PodeOAComponentResponse) function. You'll need to supply a Name, and optionally any Content/Header schemas that define the responses payload. - -The following is an example of defining a 200 response with a JSON payload of an array of objects for Name/UserId. The Response component can be used by a route referencing the name: - -```powershell -# defines a response with a json payload using New-PodeOAContentMediaType -Add-PodeOAComponentResponse -Name 'OK' -Description 'A user object' -Content ( - New-PodeOAContentMediaType -MediaType 'application/json' -Array -Content ( - New-PodeOAStringProperty -Name 'Name' | - New-PodeOAIntProperty -Name 'UserId' | - New-PodeOAObjectProperty - ) - ) - -# reuses the above response on a route using its "OK" name -Add-PodeRoute -Method Get -Path "/api/users" -ScriptBlock { - Write-PodeJsonResponse -Value @( - @{ Name = 'Rick'; UserId = 123 }, - @{ Name = 'Geralt'; UserId = 124 } - ) -} -PassThru | - Add-PodeOAResponse -StatusCode 200 -Reference 'OK' -``` - -the JSON response payload defined is as follows: - -```json -[ - { - "Name": [string], - "UserId": [integer] - } -] -``` - - -### Examples - -To define a reusable example definition you can use the [`Add-PodeOAComponentExample`](../../../Functions/OAComponents/Add-PodeOAComponentExample) function. You'll need to supply a Name, a Summary and a list of value representing the object. - -The following is an example that defines three Pet examples request bodies, and how they're used in a Route's OpenAPI definition: - -```powershell - # defines the frog example -Add-PodeOAComponentExample -name 'frog-example' -Summary "An example of a frog with a cat's name" -Value @{ - name = 'Jaguar'; petType = 'Panthera'; color = 'Lion'; gender = 'Male'; breed = 'Mantella Baroni' -} -# defines the cat example -Add-PodeOAComponentExample -Name 'cat-example' -Summary 'An example of a cat' -Value @{ - name = 'Fluffy'; petType = 'Cat'; color = 'White'; gender = 'male'; breed = 'Persian' -} -# defines the dog example -Add-PodeOAComponentExample -Name 'dog-example' -Summary "An example of a dog with a cat's name" -Value @{ - name = 'Puma'; petType = 'Dog'; color = 'Black'; gender = 'Female'; breed = 'Mixed' -} - -# reuses the examples -Add-PodeRoute -PassThru -Method Put -Path '/pet/:petId' -ScriptBlock { - # route code -} | Set-PodeOARouteInfo -Summary 'Updates a pet in the store with form data' -Tags 'pet' ` - -OperationId 'updatepet' -PassThru | - Set-PodeOARequest -Parameters @( - (New-PodeOAStringProperty -Name 'petId' -Description 'ID of pet that needs to be updated' | ConvertTo-PodeOAParameter -In Path -Required) - ) -RequestBody ( - New-PodeOARequestBody -Description 'user to add to the system' -Content @{ 'application/json' = 'Pet' } -Examples ( - New-PodeOAExample -ContentMediaType 'application/json', 'application/xml' -Reference 'cat-example' | - New-PodeOAExample -ContentMediaType 'application/json', 'application/xml' -Reference 'dog-example' | - New-PodeOAExample -ContentMediaType 'application/json', 'application/xml' -Reference 'frog-example' - ) - ) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Pet updated.' -``` - -### Headers - -To define a reusable header definition you can use the [`Add-PodeOAComponentHeader`](../../../Functions/OAComponents/Add-PodeOAComponentHeader) function. You'll need to supply a Name, and optionally any Content/Header schemas that define the responses payload. - -```powershell - # define Headers -New-PodeOAIntProperty -Format Int32 -Description 'calls per hour allowed by the user' | - Add-PodeOAComponentHeader -Name 'X-Rate-Limit' -New-PodeOAStringProperty -Format Date-Time -Description 'date in UTC when token expires' | - Add-PodeOAComponentHeader -Name 'X-Expires-After' - -Add-PodeRoute -PassThru -Method Get -Path '/user/login' -ScriptBlock { - # route code -} | Set-PodeOARouteInfo -Summary 'Logs user into the system.' -Description 'Logs user into the system.' ` - -Tags 'user' -OperationId 'loginUser' -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' ` - -Header @('X-Rate-Limit', 'X-Expires-After') -Content ( - New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'string' - ) -PassThru | - Add-PodeOAResponse -StatusCode 400 -Description 'Invalid username/password supplied' -``` - - -### CallBacks - -To define a reusable callback definition you can use the [`Add-PodeOAComponentCallBack`](../../../Functions/OAComponents/Add-PodeOAComponentCallBack) function. You'll need to supply a Name, and optionally any Content/Header schemas that define the responses payload. - -```powershell -Add-PodeRoute -PassThru -Method Post -Path '/petcallbackReference' -Authentication 'Login-OAuth2' ` - -Scope 'write' -ScriptBlock { - #route code -} | Set-PodeOARouteInfo -Summary 'Add a new pet to the store' -Description 'Add a new pet to the store' ` - -Tags 'pet' -OperationId 'petcallbackReference' -PassThru | - Set-PodeOARequest -RequestBody ( New-PodeOARequestBody -Reference 'PetBodySchema' ) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content ( - New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Pet' - ) -PassThru | - Add-PodeOAResponse -StatusCode 405 -Description 'Validation exception' -Content @{ - 'application / json' = ( New-PodeOAStringProperty -Name 'result' | - New-PodeOAStringProperty -Name 'message' | - New-PodeOAObjectProperty ) - } -PassThru | - Add-PodeOACallBack -Name 'test1' -Reference 'test' -``` - -### Response Links - -To define a reusable response link definition you can use the [`Add-PodeOAComponentResponseLink`](../../../Functions/OAComponents/Add-PodeOAComponentResponseLink) function. You'll need to supply a Name, and optionally any Content/Header schemas that define the responses payload. - -```powershell -#Add link reference -Add-PodeOAComponentResponseLink -Name 'address' -OperationId 'getUserByName' -Parameters @{ - 'username' = '$request.path.username' -} - -#use link reference -Add-PodeRoute -PassThru -Method Put -Path '/userLinkByRef/:username' -ScriptBlock { - Write-PodeJsonResponse -Value 'done' -StatusCode 200 -} | Set-PodeOARouteInfo -Summary 'Update user' -Description 'This can only be done by the logged in user.' ` - -Tags 'user' -OperationId 'updateUserLinkByRef' -PassThru | - Set-PodeOARequest -Parameters ( - ( New-PodeOAStringProperty -Name 'username' -Description ' name that need to be updated.' -Required | ConvertTo-PodeOAParameter -In Path ) - ) -RequestBody ( - New-PodeOARequestBody -Required -Content ( - New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'User' ) - ) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Content @{'application/json' = 'User' } -PassThru -Links ( - New-PodeOAResponseLink -Name 'address2' -Reference 'address' - ) | - Add-PodeOAResponse -StatusCode 400 -Description 'Invalid username supplied' -PassThru | - Add-PodeOAResponse -StatusCode 404 -Description 'User not found' -PassThru | - Add-PodeOAResponse -StatusCode 405 -Description 'Invalid Input' -``` - -## Properties - -Properties are used to create all Parameters and Schemas in OpenAPI. You can use the simple types on their own, or you can combine multiple of them together to form complex objects. - -### Simple Types - -There are 5 simple property types: Integers, Numbers, Strings, Booleans, and Schemas. Each of which can be created using the following functions: - -* [`New-PodeOAIntProperty`](../../../Functions/OAProperties/New-PodeOAIntProperty) -* [`New-PodeOANumberProperty`](../../../Functions/OAProperties/New-PodeOANumberProperty) -* [`New-PodeOAStringProperty`](../../../Functions/OAProperties/New-PodeOAStringProperty) -* [`New-PodeOABoolProperty`](../../../Functions/OAProperties/New-PodeOABoolProperty) -* [`New-PodeOASchemaProperty`](../../../Functions//New-PodeOASchemaProperty) -* [`New-PodeOAMultiTypeProperty`](../../../Functions/OAProperties/New-PodeOAMultiTypeProperty) (Note: OpenAPI 3.1 only) - -These properties can be created with a Name, and other flags such as Required and/or a Description: - -```powershell -# simple integer -New-PodeOAIntProperty -Name 'userId' - -# a float number with a max value of 100 -New-PodeOANumberProperty -Name 'ratio' -Format Float -Maximum 100 - -# a string with a default value, and enum of options -New-PodeOAStringProperty -Name 'type' -Default 'admin' -Enum @('admin', 'user') - -# a boolean that's required -New-PodeOABoolProperty -Name 'enabled' -Required - -# a schema property that references another component schema -New-PodeOASchemaProperty -Name 'Config' -Reference 'ConfigSchema' - -# a string or an integer or a null value (only available with OpenAPI 3.1) -New-PodeOAMultiTypeProperty -Name 'multi' -Type integer,string -Nullable -``` - -On their own, like above, the simple properties don't really do much. However, you can combine that together to make complex objects/arrays as defined below. - -### Arrays - -There isn't a dedicated function to create an array property, instead there is an `-Array` switch on each of the property functions - both Object and the above simple properties. - -If you supply the `-Array` switch to any of the above simple properties, this will define an array of that type - the `-Name` parameter can also be omitted if only a simple array if required. - -For example, the below will define an integer array: - -```powershell -New-PodeOAIntProperty -Array -``` - -When used in a Response, this could return the following JSON example: - -```json -[ - 0, - 1, - 2 -] -``` - -### Objects - -An object property is a combination of multiple other properties - both simple, array of more objects. - -There are two ways to define objects: - -1. Similar to arrays, you can use the `-Object` switch on the simple properties. -2. You can use the [`New-PodeOAObjectProperty`](../../../Functions/OAProperties/New-PodeOAObjectProperty) function to combine multiple properties. - -#### Simple - -If you use the `-Object` switch on the simple property function, this will automatically wrap the property as an object. The Name for this is required. - -For example, the below will define a simple `userId` integer object: - -```powershell -New-PodeOAIntProperty -Name 'userId' -Object -``` - -In a response as JSON, this could look as follows: - -```json -{ - "userId": 0 -} -``` - -Furthermore, you can also supply both `-Array` and `-Object` switches: - -```powershell -New-PodeOAIntProperty -Name 'userId' -Object -Array -``` - -This wil result in something like the following JSON: - -```json -{ - "userId": [ 0, 1, 2 ] -} -``` - -#### Complex - -Unlike the `-Object` switch that simply converts a single property into an object, the [`New-PodeOAObjectProperty`](../../../Functions/OAProperties/New-PodeOAObjectProperty) function can combine and convert multiple properties. - -For example, the following will create an object using an Integer, String, and a Boolean: - -Legacy Definition - -```powershell -New-PodeOAObjectProperty -Properties ( - (New-PodeOAIntProperty -Name 'userId'), - (New-PodeOAStringProperty -Name 'name'), - (New-PodeOABoolProperty -Name 'enabled') -) -``` - -Using piping (new in Pode 2.10) - -```powershell -New-PodeOAIntProperty -Name 'userId'| New-PodeOAStringProperty -Name 'name'| - New-PodeOABoolProperty -Name 'enabled' |New-PodeOAObjectProperty -``` - -As JSON, this could look as follows: - -```json -{ - "userId": 0, - "name": "Gary Goodspeed", - "enabled": true -} -``` - -You can also supply the `-Array` switch to the [`New-PodeOAObjectProperty`](../../../Functions/OAProperties/New-PodeOAObjectProperty) function. This will result in an array of objects. For example, if we took the above: - -```powershell -New-PodeOAIntProperty -Name 'userId'| New-PodeOAStringProperty -Name 'name'| - New-PodeOABoolProperty -Name 'enabled' |New-PodeOAObjectProperty -Array -``` - -As JSON, this could look as follows: - -```json -[ - { - "userId": 0, - "name": "Gary Goodspeed", - "enabled": true - }, - { - "userId": 1, - "name": "Kevin", - "enabled": false - } -] -``` - -You can also combine objects into other objects: - -```powershell -$usersArray = New-PodeOAIntProperty -Name 'userId'| New-PodeOAStringProperty -Name 'name'| - New-PodeOABoolProperty -Name 'enabled' |New-PodeOAObjectProperty -Array - -New-PodeOAObjectProperty -Properties @( - (New-PodeOAIntProperty -Name 'found'), - $usersArray -) -``` - -As JSON, this could look as follows: - -```json -{ - "found": 2, - "users": [ - { - "userId": 0, - "name": "Gary Goodspeed", - "enabled": true - }, - { - "userId": 1, - "name": "Kevin", - "enabled": false - } - ] -} -``` - -### oneOf, anyOf and allOf Keywords - -OpenAPI 3.x provides several keywords which you can use to combine schemas. You can use these keywords to create a complex schema or validate a value against multiple criteria. - -* oneOf - validates the value against exactly one of the sub-schemas -* allOf - validates the value against all the sub-schemas -* anyOf - validates the value against any (one or more) of the sub-schemas - -You can use the [`Merge-PodeOAProperty`](../../../Functions/OAProperties/Merge-PodeOAProperty) will instead define a relationship between the properties. - -Unlike [`New-PodeOAObjectProperty`](../../../Functions/OAProperties/New-PodeOAObjectProperty) which combines and converts multiple properties into an Object, [`Merge-PodeOAProperty`](../../../Functions/OAProperties/Merge-PodeOAProperty) will instead define a relationship between the properties. - -For example, the following will create an something like an C Union object using an Integer, String, and a Boolean: - -```powershell -Merge-PodeOAProperty -Type OneOf -ObjectDefinitions @( - (New-PodeOAIntProperty -Name 'userId' -Object), - (New-PodeOAStringProperty -Name 'name' -Object), - (New-PodeOABoolProperty -Name 'enabled' -Object) - ) -``` - -Or - -```powershell -New-PodeOAIntProperty -Name 'userId' -Object | - New-PodeOAStringProperty -Name 'name' -Object | - New-PodeOABoolProperty -Name 'enabled' -Object | - Merge-PodeOAProperty -Type OneOf -``` - -As JSON, this could look as follows: - -```json -{ - "oneOf": [ - { - "type": "object", - "properties": { - "userId": { - "type": "integer" - } - } - }, - { - "type": "object", - "properties": { - "name": { - "type": "string" - } - } - }, - { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": false - } - } - } - ] -} -``` - -You can also supply a Component Schema created using [`Add-PodeOAComponentSchema`](../../../Functions/OAComponents/Add-PodeOAComponentSchema). For example, if we took the above: - -```powershell - New-PodeOAIntProperty -Name 'id'-Format Int64 -Example 1 -ReadOnly | - New-PodeOAStringProperty -Name 'username' -Example 'theUser' -Required | - New-PodeOAStringProperty -Name 'firstName' -Example 'John' | - New-PodeOAStringProperty -Name 'lastName' -Example 'James' | - New-PodeOAStringProperty -Name 'email' -Format email -Example 'john@email.com' | - New-PodeOAStringProperty -Name 'lastName' -Example 'James' | - New-PodeOAStringProperty -Name 'password' -Format Password -Example '12345' -Required | - New-PodeOAStringProperty -Name 'phone' -Example '12345' | - New-PodeOAIntProperty -Name 'userStatus'-Format int32 -Description 'User Status' -Example 1| - New-PodeOAObjectProperty -Name 'User' -XmlName 'user' | - Add-PodeOAComponentSchema - - New-PodeOAStringProperty -Name 'street' -Example '437 Lytton' -Required | - New-PodeOAStringProperty -Name 'city' -Example 'Palo Alto' -Required | - New-PodeOAStringProperty -Name 'state' -Example 'CA' -Required | - New-PodeOAStringProperty -Name 'zip' -Example '94031' -Required | - New-PodeOAObjectProperty -Name 'Address' -XmlName 'address' -Description 'Shipping Address' | - Add-PodeOAComponentSchema - - Merge-PodeOAProperty -Type AllOf -ObjectDefinitions 'Address','User' - -``` - -As JSON, this could look as follows: - -```json -{ - "allOf": [ - { - "$ref": "#/components/schemas/Address" - }, - { - "$ref": "#/components/schemas/User" - } - ] -} -``` -## Implementing Parameter Validation - -Is possible to validate any parameter submitted by clients against an OpenAPI schema, ensuring adherence to defined standards. - - -First, schema validation has to be enabled using : - -```powershell -Enable-PodeOpenApi -EnableSchemaValidation #any other parameters needed -``` - -This command activates the OpenAPI feature with schema validation enabled, ensuring strict adherence to specified schemas. - -Next, is possible to validate any route using `PodeOAJsonSchemaCompliance`. -In this example, we'll create a route for updating a pet: - -```powershell -Add-PodeRoute -PassThru -Method Post -Path '/user' -ScriptBlock { - $contentType = Get-PodeHeader -Name 'Content-Type' - $responseMediaType = Get-PodeHeader -Name 'Accept' - switch ($contentType) { - 'application/xml' { - $user = ConvertFrom-PodeXml -node $WebEvent.data | ConvertTo-Json - } - 'application/json' { $user = ConvertTo-Json $WebEvent.data } - 'application/x-www-form-urlencoded' { $user = ConvertTo-Json $WebEvent.data } - default { - Write-PodeHtmlResponse -StatusCode 415 - return - } - } - $Validate = Test-PodeOAJsonSchemaCompliance -Json $user -SchemaReference 'User' - if ($Validate.result) { - $newUser = Add-user -User (convertfrom-json -InputObject $user -AsHashtable) - Save-PodeState -Path $using:PetDataJson - switch ($responseMediaType) { - 'application/xml' { Write-PodeXmlResponse -Value $newUser -StatusCode 200 } - 'application/json' { Write-PodeJsonResponse -Value $newUser -StatusCode 200 } - default { Write-PodeHtmlResponse -StatusCode 415 } - } - } - else { - Write-PodeHtmlResponse -StatusCode 405 -Value ($Validate.message -join ', ') - } -} | Set-PodeOARouteInfo -Summary 'Create user.' -Description 'This can only be done by the logged in user.' -Tags 'user' -OperationId 'createUser' -PassThru | - Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'User' )) -PassThru | - Add-PodeOAResponse -StatusCode 405 -Description 'Invalid Input' -PassThru | - Add-PodeOAResponse -Default -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'User' ) -``` -#### Explanation -- The route handles different content types (JSON/XML) and converts them to JSON for validation. -- It validates the received pet object against the 'User' schema using the 'Test-PodeOAJsonSchemaCompliance' function. -- Depending on the validation result, appropriate HTTP responses are returned. -- OpenAPI metadata such as summary, description, request body, and responses are also defined for documentation purposes. - - - -## OpenApi Documentation pages - -If you're not using a custom OpenAPI viewer, then you can use one or more of the inbuilt which Pode supports: ones with Pode: - -* Swagger -* ReDoc -* RapiDoc -* StopLight -* Explorer -* RapiPdf - -For each you can customise the Route path to access the page on, but by default Swagger is at `/swagger`, ReDoc is at `/redoc`, etc. If you've written your own custom OpenAPI definition then you can also set a custom Route path to fetch the definition on. - -To enable a viewer you can use the [`Enable-PodeOAViewer`](../../../Functions/OpenApi/Enable-PodeOAViewer) function: - -```powershell -# for swagger at "/docs/swagger" -Enable-PodeOpenApiViewer -Type Swagger -Path '/docs/swagger' -DarkMode - -# and ReDoc at the default "/redoc" -Enable-PodeOpenApiViewer -Type ReDoc - -# and RapiDoc at "/docs/rapidoc" -Enable-PodeOAViewer -Type RapiDoc -Path '/docs/rapidoc' -DarkMode - -# and StopLight at "/docs/stoplight" -Enable-PodeOAViewer -Type StopLight -Path '/docs/stoplight' - -# and Explorer at "/docs/explorer" -Enable-PodeOAViewer -Type Explorer -Path '/docs/explorer' - -# and RapiPdf at "/docs/rapipdf" -Enable-PodeOAViewer -Type RapiPdf -Path '/docs/rapipdf' - -# plus a bookmark page with the link to all documentation -Enable-PodeOAViewer -Bookmarks -Path '/docs' - -# there is also an OpenAPI editor (only for v3.0.x) -Enable-PodeOAViewer -Editor -Path '/docs/swagger-editor' -``` - -## Multiple OpenAPI definition - -It's possible to create multiple OpenAPI definitions inside the same Server instance. This feature could be useful in situations such as: - -* Multiple versions of the OpenAPI specification for different use cases -* The same OpenAPI definition, but one using OpenAPI v3.0.3 and another using v3.1.0 -* Different APIs based on the IP or URL - - -### How to use it -Any Pode function that interacts with OpenAPI has a `-DefinitionTag [string[]]` parameter available. This allows you to specify within which OpenAPI definition(s) the API's definition should be available. - -!!! note - These functions accept a simple string, and not an array - - * Get-PodeOADefinition - * Enable-PodeOpenApi - * Enable-PodeOAViewer - * Add-PodeOAInfo - * Test-PodeOAJsonSchemaCompliance - -A new OpenAPI definition has to be created using the `Enable-PodeOpenApi` function - -```powershell -Enable-PodeOpenApi -Path '/docs/openapi/v3.0' -OpenApiVersion '3.0.3' -DefinitionTag 'v3' -Enable-PodeOpenApi -Path '/docs/openapi/v3.1' -OpenApiVersion '3.1.0' -DefinitionTag 'v3.1' -Enable-PodeOpenApi -Path '/docs/openapi/admin' -OpenApiVersion '3.1.0' -DefinitionTag 'admin' -``` - -There is also [`Select-PodeOADefinition`](../../../Functions/OpenApi/Select-PodeOADefinition), which simplifies the selection of which OpenAPI definition to use as a wrapper around multiple OpenAPI functions, or Route functions. Meaning you don't have to specify `-DefinitionTag` on embedded OpenAPI/Route calls: - -```powershell -Select-PodeOADefinition -Tag 'v3', 'v3.1' -Scriptblock { - Add-PodeRouteGroup -Path '/api/v5' -Routes { - Add-PodeRoute -Method Get -Path '/petbyRef/:petId' -ScriptBlock { - Write-PodeJsonResponse -Value 'done' -StatusCode 2005 - } - } -} - -Select-PodeOADefinition -Tag 'admin' -ScriptBlock { - # your admin definition -} -``` - -The default `Definition Tag` is named "default". This can be changed using the `Server.psd1` file and the `Web.OpenApi.DefaultDefinitionTag` property - -```powershell -@{ - Web=@{ - OpenApi=@{ - DefaultDefinitionTag= 'NewDfault' - } - } -} -``` - -### OpenAPI example - -A simple OpenAPI definition - -```powershell -Add-PodeOAInfo -Title 'Swagger Petstore - OpenAPI 3.0' -Version 1.0.17 -Description $InfoDescription -TermsOfService 'http://swagger.io/terms/' -LicenseName 'Apache 2.0' ` - -LicenseUrl 'http://www.apache.org/licenses/LICENSE-2.0.html' -ContactName 'API Support' -ContactEmail 'apiteam@swagger.io' -DefinitionTag 'v3' - -Add-PodeOAInfo -Title 'Swagger Petstore - OpenAPI 3.1' -Version 1.0.17 -Description $InfoDescription -TermsOfService 'http://swagger.io/terms/' -LicenseName 'Apache 2.0' ` - -LicenseUrl 'http://www.apache.org/licenses/LICENSE-2.0.html' -ContactName 'API Support' -ContactEmail 'apiteam@swagger.io' -DefinitionTag 'v3.1' - -Add-PodeOAServerEndpoint -url '/api/v3' -Description 'default endpoint' -DefinitionTag 'v3', 'v3.1' - -#OpenAPI 3.0 -Enable-PodeOAViewer -Type Swagger -Path '/docs/swagger' -DefinitionTag 'v3' -Enable-PodeOAViewer -Type Bookmarks -Path '/docs' -DefinitionTag 'v3' - -#OpenAPI 3.1 -Enable-PodeOAViewer -Type Swagger -Path '/docs/v3.1/swagger' -DefinitionTag 'v3.1' -Enable-PodeOAViewer -Type ReDoc -Path '/docs/v3.1/redoc' -DarkMode -DefinitionTag 'v3.1' -Enable-PodeOAViewer -Type Bookmarks -Path '/docs/v3.1' -DefinitionTag 'v3.1' - -Select-PodeOADefinition -Tag 'v3', 'v3.1' -ScriptBlock { - New-PodeOAIntProperty -Name 'id'-Format Int64 -Example 10 -Required | - New-PodeOAStringProperty -Name 'name' -Example 'doggie' -Required | - New-PodeOASchemaProperty -Name 'category' -Reference 'Category' | - New-PodeOAStringProperty -Name 'photoUrls' -Array -XmlWrapped -XmlItemName 'photoUrl' -Required | - New-PodeOASchemaProperty -Name 'tags' -Reference 'Tag' -Array -XmlWrapped | - New-PodeOAStringProperty -Name 'status' -Description 'pet status in the store' -Enum @('available', 'pending', 'sold') | - New-PodeOAObjectProperty -XmlName 'pet' | - Add-PodeOAComponentSchema -Name 'Pet' - - - Add-PodeRouteGroup -Path '/api/v3' -Routes { - Add-PodeRoute -PassThru -Method Put -Path '/pet' -Authentication 'merged_auth_nokey' -Scope 'write:pets', 'read:pets' -ScriptBlock { - #code - } | Set-PodeOARouteInfo -Summary 'Update an existing pet' -Description 'Update an existing pet by Id' -Tags 'pet' -OperationId 'updatePet' -PassThru | - Set-PodeOARequest -RequestBody ( - New-PodeOARequestBody -Description 'Update an existent pet in the store' -Required -Content ( - New-PodeOAContentMediaType -ContentMediaType 'application/json', 'application/xml' -Content 'Pet' ) - ) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentMediaType 'application/json', 'application/xml' -Content 'Pet' ) -PassThru | - Add-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' -PassThru | - Add-PodeOAResponse -StatusCode 404 -Description 'Pet not found' -PassThru | - Add-PodeOAResponse -StatusCode 405 -Description 'Validation exception' - } -} -``` diff --git a/docs/Tutorials/OpenAPI/Specification/v3.0.3.md b/docs/Tutorials/OpenAPI/Specification/v3.0.3.md index 55274d89b..9d9ae153f 100644 --- a/docs/Tutorials/OpenAPI/Specification/v3.0.3.md +++ b/docs/Tutorials/OpenAPI/Specification/v3.0.3.md @@ -161,10 +161,10 @@ Types that are not accompanied by a `format` property follow the type definition The formats defined by the OAS are: -| [`type`](#dataTypes) | [`format`](#dataTypeFormat) | [`Pode CmdLet`](https://badgerati.github.io/Pode/Tutorials/OpenAPI/) | Comments | -| -------------------- | --------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | -| `integer` | `int32` | [`New-PodeOAIntProperty -Name 'anInteger' -Format Int32`] | signed 32 bits | -| `integer` | `int64` | [`New-PodeOAIntProperty -Name 'aLong' -Format Int64`] | signed 64 bits (a.k.a long) | +| [`type`](#dataTypes) | [`format`](#dataTypeFormat) | [`Pode CmdLet`](https://badgerati.github.io/Pode/Tutorials/OpenAPI/) | Comments | +|----------------------|-----------------------------|----------------------------------------------------------------------|-----------------------------| +| `integer` | `int32` | [`New-PodeOAIntProperty -Name 'anInteger' -Format Int32`] | signed 32 bits | +| `integer` | `int64` | [`New-PodeOAIntProperty -Name 'aLong' -Format Int64`] | signed 64 bits (a.k.a long) | | `number` | `float` | [`New-PodeOANumberProperty -Name 'aFloat' -Format Float`] | | `number` | `double` | [`New-PodeOANumberProperty -Name 'aDouble' -Format Double`] | | `string` | | [`New-PodeOAStringProperty -Name 'aString'`] | @@ -198,7 +198,7 @@ This is the root document object of the [OpenAPI document](#oasDocument). ##### Fixed Fields | Field Name | Type | Pode CmdLets | Description | -| ------------------------------------------ | :-----------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|--------------------------------------------|:-------------------------------------------------------------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | openapi | `string` | [`Enable-PodeOpenApi`](../../../../Functions/OpenApi/Enable-PodeOpenApi) | **REQUIRED**. This string MUST be the [semantic version number](https://semver.org/spec/v2.0.0.html) of the [OpenAPI Specification version](#versions) that the OpenAPI document uses. The `openapi` field SHOULD be used by tooling specifications and clients to interpret the OpenAPI document. This is *not* related to the API [`info.version`](#infoVersion) string. | | info | [Info Object](#infoObject) | [`Add-PodeOAInfo`](../../../../Functions/OpenApi/Add-PodeOAInfo) | **REQUIRED**. Provides metadata about the API. The metadata MAY be used by tooling as required. | | servers | [[Server Object](#serverObject)] | [`Add-PodeOAServerEndpoint`](../../../../Functions/OpenApi/Add-PodeOAServerEndpoint) | An array of Server Objects, which provide connectivity information to a target server. If the `servers` property is not provided, or is an empty array, the default value would be a [Server Object](#serverObject) with a [url](#serverUrl) value of `/`. | @@ -218,7 +218,7 @@ The metadata MAY be used by the clients if needed, and MAY be presented in editi ##### Fixed Fields | Field Name | Type | `Add-PodeOAInfo` | Description | -| ----------------------------------------------- | :------------------------------: | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|-------------------------------------------------|:--------------------------------:|----------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| | title | `string` | `-Title` | **REQUIRED**. The title of the API. | | description | `string` | `-Description` | A short description of the API. [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text representation. | | termsOfService | `string` | `-TermOfService` | A URL to the Terms of Service for the API. MUST be in the format of a URL. | @@ -278,7 +278,7 @@ Contact information for the exposed API. ##### Fixed Fields | Field Name | Type | `Add-PodeOAInfo` | Description | -| -------------------------------- | :------: | ---------------- | ------------------------------------------------------------------------------------------------ | +|----------------------------------|:--------:|------------------|--------------------------------------------------------------------------------------------------| | name | `string` | `-ContactName` | The identifying name of the contact person/organization. | | url | `string` | `-ContactUrl` | The URL pointing to the contact information. MUST be in the format of a URL. | | email | `string` | `-ContactEmail` | The email address of the contact person/organization. MUST be in the format of an email address. | @@ -312,7 +312,7 @@ License information for the exposed API. ##### Fixed Fields | Field Name | Type | `Add-PodeOAInfo` | Description | -| ------------------------------ | :------: | ---------------- | ---------------------------------------------------------------------- | +|--------------------------------|:--------:|------------------|------------------------------------------------------------------------| | name | `string` | `-LicenseName` | **REQUIRED**. The license name used for the API. | | url | `string` | `-LicenseUrl` | A URL to the license used for the API. MUST be in the format of a URL. | @@ -342,7 +342,7 @@ An object representing a Server. ##### Fixed Fields | Field Name | Type | `Add-PodeOAServerEndpoint` | Description | -| ------------------------------------------- | :------------------------------------------------------------: | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +|---------------------------------------------|:--------------------------------------------------------------:|----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | url | `string` | `-Url` | **REQUIRED**. A URL to the target host. This URL supports Server Variables and MAY be relative, to indicate that the host location is relative to the location where the OpenAPI document is being served. Variable substitutions will be made when a variable is named in `{`brackets`}`. | | description | `string` | `-Description` | An optional string describing the host designated by the URL. [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text representation. | | variables | Map[`string`, [Server Variable Object](#serverVariableObject)] | `-Variable` | A map between a variable name and its value. The value is used for substitution in the server's URL template. In Pode the OpenAPI Object's [`servers`](#oasServers) with variables can be defined using a `[ordered]@{}` [System.Collections.Specialized.OrderedDictionary](https://learn.microsoft.com/en-us/dotnet/api/system.collections.specialized.ordereddictionary?view=net-7.0) | @@ -481,7 +481,7 @@ An object representing a Server Variable for server URL template substitution. ##### Fixed Fields | Field Name | Type | Description | -| --------------------------------------------------- | :--------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|-----------------------------------------------------|:----------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | enum | [`string`] | An enumeration of string values to be used if the substitution options are from a limited set. The array SHOULD NOT be empty. | | default | `string` | **REQUIRED**. The default value to use for substitution, which SHALL be sent if an alternate value is _not_ supplied. Note this behavior is different than the [Schema Object's](#schemaObject) treatment of default values, because in those cases parameter values are optional. If the [`enum`](#serverVariableEnum) is defined, the value SHOULD exist in the enum's values. | | description | `string` | An optional description for the server variable. [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text representation. | @@ -496,10 +496,10 @@ All objects defined within the components object will have no effect on the API ##### Fixed Fields -| Field Name | Type | Pode | Description | -| -------------------------------------------------------- | :----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | -| schemas | Map[`string`, [Schema Object](#schemaObject) \| [Reference Object](#referenceObject)] | [`Add-PodeOAComponentSchema`](../../../../Functions/OAComponents/Add-PodeOAComponentSchema) | An object to hold reusable [Schema Objects](#schemaObject). | -| responses | Map[`string`, [Response Object](#responseObject) \| [Reference Object](#referenceObject)] | [`Add-PodeOAComponentResponse`](../../../../Functions/OAComponents/Add-PodeOAComponentResponse) | An object to hold reusable [Response Objects](#responseObject). | +| Field Name | Type | Pode | Description | +|----------------------------------------------|:------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|-----------------------------------------------------------------| +| schemas | Map[`string`, [Schema Object](#schemaObject) \| [Reference Object](#referenceObject)] | [`Add-PodeOAComponentSchema`](../../../../Functions/OAComponents/Add-PodeOAComponentSchema) | An object to hold reusable [Schema Objects](#schemaObject). | +| responses | Map[`string`, [Response Object](#responseObject) \| [Reference Object](#referenceObject)] | [`Add-PodeOAComponentResponse`](../../../../Functions/OAComponents/Add-PodeOAComponentResponse) | An object to hold reusable [Response Objects](#responseObject). | | parameters | Map[`string`, [Parameter Object](#parameterObject) \| [Reference Object](#referenceObject)] | [`Add-PodeOAComponentParameter`](../../../../Functions/OAComponents/Add-PodeOAComponentParameter) | An object to hold reusable [Parameter Objects](#parameterObject). | PodeOAComponentExample | | examples | Map[`string`, [Example Object](#exampleObject) \| [Reference Object](#referenceObject)] | [`Add-PodeOAComponentExample`](../../../../Functions/OAComponents/Add-PodeOAComponentExample) | An object to hold reusable [Example Objects](#exampleObject). | | requestBodies | Map[`string`, [Request Body Object](#requestBodyObject) \| [Reference Object](#referenceObject)] | [`Add-PodeOAComponentRequestBody`](../../../../Functions/OAComponents/Add-PodeOAComponentRequestBody) | An object to hold reusable [Request Body Objects](#requestBodyObject). | @@ -752,7 +752,7 @@ The path is appended to the URL from the [`Server Object`](#serverObject) in ord ##### Patterned Fields | Field Pattern | Type | Description | -| ------------------------------- | :---------------------------------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|---------------------------------|:-----------------------------------:|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | /{path} | [Path Item Object](#pathItemObject) | A relative path to an individual endpoint. The field name MUST begin with a forward slash (`/`). The path is **appended** (no relative URL resolution) to the expanded URL from the [`Server Object`](#serverObject)'s `url` field in order to construct the full URL. [Path templating](#pathTemplating) is allowed. When matching URLs, concrete (non-templated) paths would be matched before their templated counterparts. Templated paths with the same hierarchy but different templated names MUST NOT exist as they are identical. In case of ambiguous matching, it's up to the tooling to decide which one to use. | This object MAY be extended with [Specification Extensions](#specificationExtensions). @@ -839,7 +839,7 @@ The path itself is still exposed to the documentation viewer but they will not k ##### Fixed Fields | Field Name | Type | Description | -| --------------------------------------------- | :----------------------------------------------------------------------------: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|-----------------------------------------------|:------------------------------------------------------------------------------:|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | $ref | `string` | (Unsupported by Pode) Allows for an external definition of this path item. The referenced structure MUST be in the format of a [Path Item Object](#pathItemObject). In case a Path Item Object field appears both in the defined object and the referenced object, the behavior is undefined. | | summary | `string` | An optional, string summary, intended to apply to all operations in this path. | | description | `string` | An optional, string description, intended to apply to all operations in this path. [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text representation. | @@ -1096,7 +1096,7 @@ $Route = Add-PodeRoute -PassThru -Method Get -Path '/pet/:petId' -ScriptBlock { ``` | Field Name | Type | `Set-PodeOARouteInfo` | Description | -| ------------------------------------------------ | :---------------------------------------------------------------------------------------: | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +|--------------------------------------------------|:-----------------------------------------------------------------------------------------:|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | tags | `string` | `-Tags` | A list of tags for API documentation control. Tags can be used for logical grouping of operations by resources or any other qualifier. | | summary | `string` | `-Summary` | A short summary of what the operation does. | | description | `string` | `-Description` | A verbose explanation of the operation behavior. [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text representation. | @@ -1107,25 +1107,25 @@ $Route = Add-PodeRoute -PassThru -Method Get -Path '/pet/:petId' -ScriptBlock { | servers | [[Server Object](#serverObject)] | TBD | An alternative `server` array to service this operation. If an alternative `server` object is specified at the Path Item Object or Root level, it will be overridden by this value. | | Field Name | Type | `Set-PodeOARequest` | Description | -| ---------------------------------------------- | :-------------------------------------------------------------------------------: | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|------------------------------------------------|:---------------------------------------------------------------------------------:|---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | parameters | [[Parameter Object](#parameterObject) \| [Reference Object](#referenceObject)] | `-Parameters` | A list of parameters that are applicable for this operation. If a parameter is already defined at the [Path Item](#pathItemParameters), the new definition will override it but can never remove it. The list MUST NOT include duplicated parameters. A unique parameter is defined by a combination of a [name](#parameterName) and [location](#parameterIn). The list can use the [Reference Object](#referenceObject) to link to parameters that are defined at the [OpenAPI Object's components/parameters](#componentsParameters). | | requestBody | [Request Body Object](#requestBodyObject) \| [Reference Object](#referenceObject) | `-RequestBody` | The request body applicable for this operation. The `requestBody` is only supported in HTTP methods where the HTTP 1.1 specification [RFC7231](https://tools.ietf.org/html/rfc7231#section-4.3.1) has explicitly defined semantics for request bodies. In other cases where the HTTP spec is vague, `requestBody` SHALL be ignored by consumers. | | Field Name | Type | `Set-PodeOAResponse` | Description | -| ------------------------------------------ | :----------------------------------: | -------------------- | ------------------------------------------------------------------------------------------------ | +|--------------------------------------------|:------------------------------------:|----------------------|--------------------------------------------------------------------------------------------------| | responses | [Responses Object](#responsesObject) | | **REQUIRED**. The list of possible responses as they are returned from executing this operation. | | Field Name | Type | `Add-PodeRoute` | Description | -| ---------------------------------------- | :---------------------------------------------------------: | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|------------------------------------------|:-----------------------------------------------------------:|-------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | security | [[Security Requirement Object](#securityRequirementObject)] | `-Authentication` `-Scope ` | A declaration of which security mechanisms can be used for this operation. The list of values includes alternative security requirement objects that can be used. Only one of the security requirement objects need to be satisfied to authorize a request. To make security optional, an empty security requirement (`{}`) can be included in the array. This definition overrides any declared top-level [`security`](#oasSecurity). To remove a top-level security declaration, an empty array can be used. | | Field Name | Type | Unsupported | Description | -| ------------------------------------------ | :---------------------------------------------------------------------------------------: | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|--------------------------------------------|:-----------------------------------------------------------------------------------------:|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | callbacks | Map[`string`, [Callback Object](#callbackObject) \| [Reference Object](#referenceObject)] | | A map of possible out-of band callbacks related to the parent operation. The key is a unique identifier for the Callback Object. Each value in the map is a [Callback Object](#callbackObject) that describes a request that may be initiated by the API provider and the expected responses. | | servers | [[Server Object](#serverObject)] | | An alternative `server` array to service this operation. If an alternative `server` object is specified at the Path Item Object or Root level, it will be overridden by this value. | @@ -1275,7 +1275,7 @@ Allows referencing an external resource for extended documentation. ##### Fixed Fields | Field Name | Type | `New-PodeOAExternalDoc` | Description | -| ------------------------------------------------ | :------: | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +|--------------------------------------------------|:--------:|-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------| | description | `string` | `-Description` | A short description of the target documentation. [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text representation. | | url | `string` | `-Url` | **REQUIRED**. The URL for the target documentation. Value MUST be in the format of a URL. | @@ -1317,7 +1317,7 @@ There are four possible parameter locations specified by the `in` field: ##### Fixed Fields | Field Name | Type | `ConvertTo-PodeOAParameter` | Description | -| ------------------------------------------------------- | :-------: | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +|---------------------------------------------------------|:---------:|-----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | name | `string` | `-Name` | **REQUIRED**. The name of the parameter. Parameter names are *case sensitive*. Note. In Pode if the -Name parameter is not used the name of the Property created by `New-PodeOAIntProperty`, `New-PodeOANumberProperty`, `New-PodeOABoolProperty `, `New-PodeOAStringProperty`, `New-PodeOAObjectProperty` is used. | | in | `string` | `-In` | **REQUIRED**. The location of the parameter. Possible values are `"query"`, `"header"`, `"path"` or `"cookie"`. | | description | `string` | `-Description` | A brief description of the parameter. This could contain examples of use. [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text representation. | @@ -1329,7 +1329,7 @@ The rules for serialization of the parameter are specified in one of two ways. For simpler scenarios, a [`schema`](#parameterSchema) and [`style`](#parameterStyle) can describe the structure and syntax of the parameter. | Field Name | Type | `ConvertTo-PodeOAParameter` | Description | -| -------------------------------------------------- | :--------------------------------------------------------------------------------------: | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|----------------------------------------------------|:----------------------------------------------------------------------------------------:|-----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | style | `string` | `-Style` | Describes how the parameter value will be serialized depending on the type of the parameter value. Default values (based on value of `in`): for `query` - `form`; for `path` - `simple`; for `header` - `simple`; for `cookie` - `form`. | | explode | `boolean` | `-Explode` | When this is true, parameter values of type `array` or `object` generate separate parameters for each value of the array or key-value pair of the map. For other types of parameters this property has no effect. When [`style`](#parameterStyle) is `form`, the default value is `true`. For all other styles, the default value is `false`. | | allowReserved | `boolean` | `-AllowReserved` | Determines whether the parameter value SHOULD allow reserved characters, as defined by [RFC3986](https://tools.ietf.org/html/rfc3986#section-2.2) `:/?#[]@!$&'()*+,;=` to be included without percent-encoding. This property only applies to parameters with an `in` value of `query`. The default value is `false`. | @@ -1343,7 +1343,7 @@ When `example` or `examples` are provided in conjunction with the `schema` objec | Field Name | Type | `ConvertTo-PodeOAParameter` | Description | -| -------------------------------------- | :--------------------------------------------------: | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +|----------------------------------------|:----------------------------------------------------:|-----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------| | content | Map[`string`, [Media Type Object](#mediaTypeObject)] | `-Content` | A map containing the representations for the parameter. The key is the media type and the value describes it. The map MUST only contain one entry. | ##### Style Values @@ -1351,7 +1351,7 @@ When `example` or `examples` are provided in conjunction with the `schema` objec In order to support common ways of serializing simple parameters, a set of `style` values are defined. | `style` | [`type`](#dataTypes) | `in` | Comments | -| -------------- | ------------------------------ | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|----------------|--------------------------------|-------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | matrix | `primitive`, `array`, `object` | `path` | Path-style parameters defined by [RFC6570](https://tools.ietf.org/html/rfc6570#section-3.2.7) | | label | `primitive`, `array`, `object` | `path` | Label style parameters defined by [RFC6570](https://tools.ietf.org/html/rfc6570#section-3.2.5) | | form | `primitive`, `array`, `object` | `query`, `cookie` | Form style parameters defined by [RFC6570](https://tools.ietf.org/html/rfc6570#section-3.2.8). This option replaces `collectionFormat` with a `csv` (when `explode` is false) or `multi` (when `explode` is true) value from OpenAPI 2.0. | @@ -1372,7 +1372,7 @@ Assume a parameter named `color` has one of the following values: The following table shows examples of rendering differences for each value. | [`style`](#dataTypeFormat) | `explode` | `empty` | `string` | `array` | `object` | -| -------------------------- | --------- | ------- | ----------- | ----------------------------------- | -------------------------------------- | +|----------------------------|-----------|---------|-------------|-------------------------------------|----------------------------------------| | matrix | false | ;color | ;color=blue | ;color=blue,black,brown | ;color=R,100,G,200,B,150 | | matrix | true | ;color | ;color=blue | ;color=blue;color=black;color=brown | ;R=100;G=200;B=150 | | label | false | . | .blue | .blue.black.brown | .R.100.G.200.B.150 | @@ -1575,7 +1575,7 @@ Describes a single request body. ##### Fixed Fields | Field Name | Type | `New-PodeOARequestBody` | Description | -| ------------------------------------------------ | :--------------------------------------------------: | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|--------------------------------------------------|:----------------------------------------------------:|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | description | `string` | `-Description` | A brief description of the request body. This could contain examples of use. [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text representation. | | content | Map[`string`, [Media Type Object](#mediaTypeObject)] | `-Content` | **REQUIRED**. The content of the request body. The key is a media type or [media type range](https://tools.ietf.org/html/rfc7231#appendix-D) and the value describes it. For requests that match multiple keys, only the most specific key is applicable. e.g. text/plain overrides text/* | | required | `boolean` | `-Required` | Determines if the request body is required in the request. Defaults to `false`. | @@ -1587,10 +1587,10 @@ This object MAY be extended with [Specification Extensions](#specificationExtens A request body with a referenced model definition. ```powershell New-PodeOARequestBody -Description 'user to add to the system' -Content @{ 'application/json' = 'User'; 'application/xml' = 'User'} -Examples ( - New-PodeOAExample -MediaType 'application/json' -Name 'user' -Summary 'User Example' -ExternalValue 'http://foo.bar/examples/user-example.json' | - New-PodeOAExample -MediaType 'application/xml' -Name 'user' -Summary 'User Example in XML' -ExternalValue 'http://foo.bar/examples/user-example.xml' | - New-PodeOAExample -MediaType 'text/plain' -Name 'user' -Summary 'User Example in Plain text' -ExternalValue 'http://foo.bar/examples/user-example.txt' | - New-PodeOAExample -MediaType '*/*' -Name 'user' -Summary 'User example in other format' -ExternalValue 'http://foo.bar/examples/user-example.whatever' + New-PodeOAExample -ContentType 'application/json' -Name 'user' -Summary 'User Example' -ExternalValue 'http://foo.bar/examples/user-example.json' | + New-PodeOAExample -ContentType 'application/xml' -Name 'user' -Summary 'User Example in XML' -ExternalValue 'http://foo.bar/examples/user-example.xml' | + New-PodeOAExample -ContentType 'text/plain' -Name 'user' -Summary 'User Example in Plain text' -ExternalValue 'http://foo.bar/examples/user-example.txt' | + New-PodeOAExample -ContentType '*/*' -Name 'user' -Summary 'User example in other format' -ExternalValue 'http://foo.bar/examples/user-example.whatever' ) ``` @@ -1709,7 +1709,7 @@ Each Media Type Object provides schema and examples for the media type identifie ##### Fixed Fields | Field Name | Type | `New-PodeOARequestBody` | Description | -| ---------------------------------------- | :--------------------------------------------------------------------------------------: | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|------------------------------------------|:----------------------------------------------------------------------------------------:|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | schema | [Schema Object](#schemaObject) \| [Reference Object](#referenceObject) | `-Schema` | The schema defining the content of the request, response, or parameter. | | example | Any | `Not Supported` | Example of the media type. The example object SHOULD be in the correct format as specified by the media type. The `example` field is mutually exclusive of the `examples` field. Furthermore, if referencing a `schema` which contains an example, the `example` value SHALL _override_ the example provided by the schema. | | examples | Map[ `string`, [Example Object](#exampleObject) \| [Reference Object](#referenceObject)] | `-Examples` | Examples of the media type. Each example object SHOULD match the media type and specified schema if present. The `examples` field is mutually exclusive of the `example` field. Furthermore, if referencing a `schema` which contains an example, the `examples` value SHALL _override_ the example provided by the schema. | @@ -1721,9 +1721,9 @@ This object MAY be extended with [Specification Extensions](#specificationExtens ```powershell New-PodeOARequestBody -Content @{ 'application/json' = 'Pet' } -Examples ( - New-PodeOAExample -MediaType 'application/json' -Name 'cat' -Summary 'An example of a cat' -Value @{name = 'Fluffy'; petType = 'Cat'; color = 'White'; gender = 'male'; breed = 'Persian' } | - New-PodeOAExample -MediaType 'application/json' -Name 'dog' -Summary "An example of a dog with a cat's name" -Value @{name = 'Puma'; petType = 'Dog'; color = 'Black'; gender = 'Female'; breed = 'Mixed' }| - New-PodeOAExample -MediaType 'application/json' -Reference 'frog-example' + New-PodeOAExample -ContentType 'application/json' -Name 'cat' -Summary 'An example of a cat' -Value @{name = 'Fluffy'; petType = 'Cat'; color = 'White'; gender = 'male'; breed = 'Persian' } | + New-PodeOAExample -ContentType 'application/json' -Name 'dog' -Summary "An example of a dog with a cat's name" -Value @{name = 'Puma'; petType = 'Dog'; color = 'Black'; gender = 'Female'; breed = 'Mixed' }| + New-PodeOAExample -ContentType 'application/json' -Reference 'frog-example' ) ``` @@ -1834,7 +1834,7 @@ In addition, specific media types MAY be specified: or ```powershell -New-PodeOAContentMediaType -MediaType 'image/jpeg','image/png' -Content (New-PodeOAStringProperty -Format binary) +New-PodeOAContentMediaType -ContentType 'image/jpeg','image/png' -Content (New-PodeOAStringProperty -Format binary) ``` ```yaml @@ -1933,7 +1933,7 @@ Examples: ```powershell Set-PodeOARequest -RequestBody ( New-PodeOARequestBody -Content ( - New-PodeOAContentMediaType -MediaType 'multipart/form-data' -Content ( + New-PodeOAContentMediaType -ContentType 'multipart/form-data' -Content ( New-PodeOAStringProperty -name 'id' -format 'uuid' | New-PodeOAObjectProperty -name 'address' -NoProperties | New-PodeOAStringProperty -name 'children' -array | @@ -1982,7 +1982,7 @@ A single encoding definition applied to a single schema property. ##### Fixed Fields | Field Name | Type | `New-PodeOAEncodingObject` | Description | -| ------------------------------------------------- | :-----------------------------------------------------------------------------------: | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|---------------------------------------------------|:-------------------------------------------------------------------------------------:|----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | contentType | `string` | `-ContentType` | The Content-Type for encoding a specific property. Default value depends on the property type: for `string` with `format` being `binary` ? `application/octet-stream`; for other primitive types ? `text/plain`; for `object` - `application/json`; for `array` ? the default is defined based on the inner type. The value can be a specific media type (e.g. `application/json`), a wildcard media type (e.g. `image/*`), or a comma-separated list of the two types. | | headers | Map[`string`, [Header Object](#headerObject) \| [Reference Object](#referenceObject)] | `-Headers` | A map allowing additional information to be provided as headers, for example `Content-Disposition`. `Content-Type` is described separately and SHALL be ignored in this section. This property SHALL be ignored if the request body media type is not a `multipart`. | | style | `string` | `-Style` | Describes how a specific property value will be serialized depending on its type. See [Parameter Object](#parameterObject) for details on the [`style`](#parameterStyle) property. The behavior follows the same values as `query` parameters, including default values. This property SHALL be ignored if the request body media type is not `application/x-www-form-urlencoded`. | @@ -1994,7 +1994,7 @@ This object MAY be extended with [Specification Extensions](#specificationExtens ##### Encoding Object Example ```powershell -New-PodeOARequestBody -Content (New-PodeOAContentMediaType -MediaType 'multipart/mixed' -Content ( +New-PodeOARequestBody -Content (New-PodeOAContentMediaType -ContentType 'multipart/mixed' -Content ( New-PodeOAStringProperty -name 'id' -format 'uuid' | New-PodeOAObjectProperty -name 'address' -NoProperties | New-PodeOAObjectProperty -name 'historyMetadata' -Description 'metadata in XML format' -NoProperties | @@ -2063,12 +2063,12 @@ SHOULD be the response for a successful operation call. ##### Fixed Fields | Field Name | Type | `Add-PodeOAResponse` | Description | -| -------------------------------------- | :------------------------------------------------------------------------: | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|----------------------------------------|:--------------------------------------------------------------------------:|----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | default | [Response Object](#responseObject) \| [Reference Object](#referenceObject) | `-Default` | The documentation of responses other than the ones declared for specific HTTP response codes. Use this field to cover undeclared responses. A [Reference Object](#referenceObject) can link to a response that the [OpenAPI Object's components/responses](#componentsResponses) section defines. | ##### Patterned Fields | Field Pattern | Type | `Add-PodeOAResponse` | Description | -| ---------------------------------------------------------- | :------------------------------------------------------------------------: | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|------------------------------------------------------------|:--------------------------------------------------------------------------:|----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [HTTP Status Code](#httpCodes) | [Response Object](#responseObject) \| [Reference Object](#referenceObject) | `-StatusCode` | Any [HTTP status code](#httpCodes) can be used as the property name, but only one property per code, to describe the expected response for that HTTP status code. A [Reference Object](#referenceObject) can link to a response that is defined in the [OpenAPI Object's components/responses](#componentsResponses) section. This field MUST be enclosed in quotation marks (for example, "200") for compatibility between JSON and YAML. To define a range of response codes, this field MAY contain the uppercase wildcard character `X`. For example, `2XX` represents all response codes between `[200-299]`. Only the following range definitions are allowed: `1XX`, `2XX`, `3XX`, `4XX`, and `5XX`. If a response is defined using an explicit code, the explicit code definition takes precedence over the range definition for that code. | @@ -2129,7 +2129,7 @@ Describes a single response from an API Operation, including design-time, static ##### Fixed Fields | Field Name | Type | `Add-PodeOAResponse` | Description | -| --------------------------------------------- | :------------------------------------------------------------------------------------: | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|-----------------------------------------------|:--------------------------------------------------------------------------------------:|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | description | `string` | `-Description` | **REQUIRED**. A short description of the response. [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text representation. | | headers | Map[`string`, [Header Object](#headerObject) \| [Reference Object](#referenceObject)] | `-Headers` | Maps a header name to its definition. [RFC7230](https://tools.ietf.org/html/rfc7230#page-22) states header names are case insensitive. If a response header is defined with the name `"Content-Type"`, it SHALL be ignored. | | content | Map[`string`, [Media Type Object](#mediaTypeObject)] | `-Content` | A map containing descriptions of potential response payloads. The key is a media type or [media type range](https://tools.ietf.org/html/rfc7231#appendix-D) and the value describes it. For responses that match multiple keys, only the most specific key is applicable. e.g. text/plain overrides text/* | @@ -2143,7 +2143,7 @@ Response of an array of a complex type: ```powershell Add-PodeOAResponse -StatusCode 200 -Description 'A complex object array response' -Content ( - New-PodeOAMediaContentType -MediaType 'application/json' -Content 'VeryComplexType' -Array + New-PodeOAMediaContentType -ContentType 'application/json' -Content 'VeryComplexType' -Array ) ``` @@ -2179,13 +2179,13 @@ Add-PodeOAResponse -StatusCode 200 -Description 'A complex object array response Response with a string type: ```powershell -Add-PodeOAResponse -StatusCode 200 -Description 'A simple string response' -Content (New-PodeOAMediaContentType -MediaType 'text/plain' -Content (New-PodeOAStringProperty) -Array) +Add-PodeOAResponse -StatusCode 200 -Description 'A simple string response' -Content (New-PodeOAMediaContentType -ContentType 'text/plain' -Content (New-PodeOAStringProperty) -Array) ``` or ```powershell -Add-PodeOAResponse -StatusCode 200 -Description 'A simple string response' -Content (New-PodeOAMediaContentType -MediaType 'text/plain' -Content 'string' -Array) +Add-PodeOAResponse -StatusCode 200 -Description 'A simple string response' -Content (New-PodeOAMediaContentType -ContentType 'text/plain' -Content 'string' -Array) ``` ```json @@ -2310,7 +2310,7 @@ The key value used to identify the path item object is an expression, evaluated ##### Patterned Fields | Field Pattern | Type | Description | -| --------------------------------------------- | :---------------------------------: | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +|-----------------------------------------------|:-----------------------------------:|----------------------------------------------------------------------------------------------------------------------------------------------------------| | {expression} | [Path Item Object](#pathItemObject) | A Path Item Object used to define a callback request and expected responses. A [complete example](../examples/v3.0/callback-example.yaml) is available. | This object MAY be extended with [Specification Extensions](#specificationExtensions). @@ -2346,7 +2346,7 @@ Location: http://example.org/subscription/1 The following examples show how the various expressions evaluate, assuming the callback operation has a path parameter named `eventType` and a query parameter named `queryUrl`. | Expression | Value | -| ---------------------------- | :--------------------------------------------------------------------------------- | +|------------------------------|:-----------------------------------------------------------------------------------| | $url | http://example.org/subscribe/myevent?queryUrl=http://clientdomain.com/stillrunning | | $method | POST | | $request.path.eventType | myevent | @@ -2404,7 +2404,7 @@ transactionCallback: ##### Fixed Fields | Field Name | Type | `New-PodeOAExample ` | Description | -| ------------------------------------------------ | :------: | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|--------------------------------------------------|:--------:|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | summary | `string` | `-Summary` | Short description for the example. | | description | `string` | `-Description` | Long description for the example. [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text representation. | | value | Any | `-Value` | Embedded literal example. The `value` field and `externalValue` field are mutually exclusive. To represent examples of media types that cannot naturally represented in JSON or YAML, use a string value to contain the example, escaping where necessary. | @@ -2422,10 +2422,10 @@ In a request body: ```powershell New-PodeOARequestBody -Content @{ 'application/json' = 'Address' } -Examples ( - New-PodeOAExample -MediaType 'application/json' -Name 'foo' -Summary 'A foo example' -Value @{foo = 'bar' } | - New-PodeOAExample -MediaType 'application/json' -Name 'bar' -Summary 'A bar example' -Value @{'bar' = 'baz' }| - New-PodeOAExample -MediaType 'application/xml' -Name 'xmlExample' -Summary 'This is an example in XML' -ExternalValue 'http://example.org/examples/address-example.xml' | - New-PodeOAExample -MediaType 'text/plain' -Name 'textExample' -Summary 'This is an example' -ExternalValue 'http://example.org/examples/address-example.txt' | + New-PodeOAExample -ContentType 'application/json' -Name 'foo' -Summary 'A foo example' -Value @{foo = 'bar' } | + New-PodeOAExample -ContentType 'application/json' -Name 'bar' -Summary 'A bar example' -Value @{'bar' = 'baz' }| + New-PodeOAExample -ContentType 'application/xml' -Name 'xmlExample' -Summary 'This is an example in XML' -ExternalValue 'http://example.org/examples/address-example.xml' | + New-PodeOAExample -ContentType 'text/plain' -Name 'textExample' -Summary 'This is an example' -ExternalValue 'http://example.org/examples/address-example.txt' | ) ``` @@ -2503,7 +2503,7 @@ For computing links, and providing instructions to execute them, a [runtime expr ##### Fixed Fields | Field Name | Type | Description | -| ------------------------------------------- | :------------------------------------------------------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|---------------------------------------------|:--------------------------------------------------------:|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | operationRef | `string` | A relative or absolute URI reference to an OAS operation. This field is mutually exclusive of the `operationId` field, and MUST point to an [Operation Object](#operationObject). Relative `operationRef` values MAY be used to locate an existing [Operation Object](#operationObject) in the OpenAPI definition. | | operationId | `string` | The name of an _existing_, resolvable OAS operation, as defined with a unique `operationId`. This field is mutually exclusive of the `operationRef` field. | | parameters | Map[`string`, Any \| [{expression}](#runtimeExpression)] | A map representing parameters to pass to an operation as specified with `operationId` or identified via `operationRef`. The key is the parameter name to be used, whereas the value can be a constant or an expression to be evaluated and passed to the linked operation. The parameter name can be qualified using the [parameter location](#parameterIn) `[{in}.]{name}` for operations that use the same parameter name in different locations (e.g. path.id). | @@ -2649,9 +2649,9 @@ The table below provides examples of runtime expressions and examples of their u ##### Examples -| Source Location | example expression | notes | -| --------------------- | :------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- | -| HTTP Method | `$method` | The allowable values for the `$method` will be those for the HTTP operation. | +| Source Location | example expression | notes | +|-----------------|:-------------------|:-----------------------------------------------------------------------------| +| HTTP Method | `$method` | The allowable values for the `$method` will be those for the HTTP operation. | | Requested media type | `$request.header.accept` | | Request parameter | `$request.path.id` | Request parameters MUST be declared in the `parameters` section of the parent operation or they cannot be evaluated. This includes request headers. | | Request body property | `$request.body#/user/uuid` | In operations which accept payloads, references may be made to portions of the `requestBody` or the entire body. | @@ -2696,7 +2696,7 @@ It is not mandatory to have a Tag Object per tag defined in the Operation Object ##### Fixed Fields | Field Name | Type | `Add-PodeOATag` | Description | -| ------------------------------------------ | :-----------------------------------------------------------: | --------------- | ---------------------------------------------------------------------------------------------------------------------------- | +|--------------------------------------------|:-------------------------------------------------------------:|-----------------|------------------------------------------------------------------------------------------------------------------------------| | name | `string` | `-Name` | **REQUIRED**. The name of the tag. | | description | `string` | `-Description` | A short description for the tag. [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text representation. | | externalDocs | [External Documentation Object](#externalDocumentationObject) | `-ExternalDocs` | Additional external documentation for this tag. In Pode is an ExternalDoc object created with `New-PodeOAExternalDoc` | @@ -2730,7 +2730,7 @@ For this specification, reference resolution is accomplished as defined by the J ##### Fixed Fields | Field Name | Type | Description | -| ------------------------------- | :------: | ----------------------------------- | +|---------------------------------|:--------:|-------------------------------------| | $ref | `string` | **REQUIRED**. The reference string. | This object cannot be extended with additional properties and any properties added SHALL be ignored. @@ -2807,7 +2807,7 @@ The following properties are taken directly from the JSON Schema definition and type - Value MUST be a string. Multiple types via an array are not supported. | type | cmdlet | -| ------ | -------------------------- | +|--------|----------------------------| | int | `New-PodeOAIntProperty` | | string | `New-PodeOAStringProperty` | | object | `New-PodeOAObjectProperty` | @@ -2839,7 +2839,7 @@ type - Value MUST be a string. Multiple types via an array are not supported. | properties | `-properties` | `New-PodeOAObjectProperty` Property definitions MUST be a [Schema Object](#schemaObject) and not a standard JSON Schema (inline or referenced). | | type | `Merge-PodeOAProperty` | Note | -| ----------------------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|-------------------------------------------------|------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | allOf | `-Type AllOf` | | | oneOf | `-Type OneOf` | (Pode) Doesn't support schema validation | | anyOf | `-Type AnyOf` | (Pode)Doesn't support schema validation | @@ -2862,7 +2862,7 @@ Other than the JSON Schema subset fields, the following fields MAY be used for f ##### Fixed Fields | Field Name | Type | | Description | -| --------------------------------------------- | :-----------------------------------------------------------: | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|-----------------------------------------------|:-------------------------------------------------------------:|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | nullable | `boolean` | `-Nullable` | A `true` value adds `"null"` to the allowed type specified by the `type` keyword, only if `type` is explicitly defined within the same Schema Object. Other Schema Object constraints retain their defined behavior, and therefore may disallow the use of `null` as a value. A `false` value leaves the specified or default `type` unmodified. The default value is `false`. | | readOnly | `boolean` | `-ReadOnly` | Relevant only for Schema `"properties"` definitions. Declares the property as "read only". This means that it MAY be sent as part of a response but SHOULD NOT be sent as part of the request. If the property is marked as `readOnly` being `true` and is in the `required` list, the `required` will take effect on the response only. A property MUST NOT be marked as both `readOnly` and `writeOnly` being `true`. Default value is `false`. | | writeOnly | `boolean` | `-WriteOnly` | Relevant only for Schema `"properties"` definitions. Declares the property as "write only". Therefore, it MAY be sent as part of a request but SHOULD NOT be sent as part of the response. If the property is marked as `writeOnly` being `true` and is in the `required` list, the `required` will take effect on the request only. A property MUST NOT be marked as both `readOnly` and `writeOnly` being `true`. Default value is `false`. | @@ -3279,7 +3279,7 @@ When using the discriminator, _inline_ schemas will not be considered. ##### Fixed Fields | Field Name | Type | `New-PodeOAObjectProperty` | Description | -| ------------------------------------------- | :---------------------: | -------------------------- | --------------------------------------------------------------------------------------------- | +|---------------------------------------------|:-----------------------:|----------------------------|-----------------------------------------------------------------------------------------------| | propertyName | `string` | `-DiscriminatorProperty` | **REQUIRED**. The name of the property in the payload that will hold the discriminator value. | | mapping | Map[`string`, `string`] | `-DiscriminatorMapping` | An object to hold mappings between payload values and schema names or references. | @@ -3442,7 +3442,7 @@ See examples for expected behavior. ##### Fixed Fields | Field Name | Type | `New-PodeOA(*)Property` | Description | -| ------------------------------------ | :-------: | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|--------------------------------------|:---------:|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | name | `string` | `-XmlName` | Replaces the name of the element/attribute used for the described schema property. When defined within `items`, it will affect the name of the individual XML elements within the list. When defined alongside `type` being `array` (outside the `items`), it will affect the wrapping element and only if `wrapped` is `true`. If `wrapped` is `false`, it will be ignored. | | namespace | `string` | `-XmlNameSpace` | The URI of the namespace definition. Value MUST be in the form of an absolute URI. | | prefix | `string` | `-XmlPrefix` | The prefix to be used for the [name](#xmlName). | @@ -3832,7 +3832,7 @@ Supported schemes are HTTP authentication, an API key (either as a header, a coo ##### Fixed Fields | Field Name | Type | Applies To | Description | -| ------------------------------------------------------------- | :-------------------------------------: | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|---------------------------------------------------------------|:---------------------------------------:|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | type | `string` | Any | **REQUIRED**. The type of the security scheme. Valid values are `"apiKey"`, `"http"`, `"oauth2"`, `"openIdConnect"`. | | description | `string` | Any | A short description for security scheme. [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text representation. | | name | `string` | `apiKey` | **REQUIRED**. The name of the header, query or cookie parameter to be used. | @@ -3962,7 +3962,7 @@ Allows configuration of the supported OAuth Flows. ##### Fixed Fields | Field Name | Type | Description | -| ----------------------------------------------------------- | :-----------------------------------: | ----------------------------------------------------------------------------------------------------- | +|-------------------------------------------------------------|:-------------------------------------:|-------------------------------------------------------------------------------------------------------| | implicit | [OAuth Flow Object](#oauthFlowObject) | Configuration for the OAuth Implicit flow | | password | [OAuth Flow Object](#oauthFlowObject) | Configuration for the OAuth Resource Owner Password flow | | clientCredentials | [OAuth Flow Object](#oauthFlowObject) | Configuration for the OAuth Client Credentials flow. Previously called `application` in OpenAPI 2.0. | @@ -3976,7 +3976,7 @@ Configuration details for a supported OAuth Flow ##### Fixed Fields | Field Name | Type | Applies To | Description | -| -------------------------------------------------------- | :---------------------: | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +|----------------------------------------------------------|:-----------------------:|-----------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------| | authorizationUrl | `string` | `oauth2` (`"implicit"`, `"authorizationCode"`) | **REQUIRED**. The authorization URL to be used for this flow. This MUST be in the form of a URL. | | tokenUrl | `string` | `oauth2` (`"password"`, `"clientCredentials"`, `"authorizationCode"`) | **REQUIRED**. The token URL to be used for this flow. This MUST be in the form of a URL. | | refreshUrl | `string` | `oauth2` | The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. | @@ -4038,7 +4038,7 @@ When a list of Security Requirement Objects is defined on the [OpenAPI Object](# ##### Patterned Fields | Field Pattern | Type | Description | -| --------------------------------------------- | :--------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|-----------------------------------------------|:----------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | {name} | [`string`] | Each name MUST correspond to a security scheme which is declared in the [Security Schemes](#componentsSecuritySchemes) under the [Components Object](#componentsObject). If the security scheme is of type `"oauth2"` or `"openIdConnect"`, then the value is a list of scope names required for the execution, and the list MAY be empty if authorization does not require a specified scope. For other security scheme types, the array MUST be empty. | ##### Security Requirement Object Examples @@ -4227,9 +4227,9 @@ While the OpenAPI Specification tries to accommodate most use cases, additional The extensions properties are implemented as patterned fields that are always prefixed by `"x-"`. -| Field Pattern | Type | Description | -| -------------------------------- | :---: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| ^x- | Any | Allows extensions to the OpenAPI Schema. The field name MUST begin with `x-`, for example, `x-internal-id`. The value can be `null`, a primitive, an array or an object. Can have any valid JSON format value. | +| Field Pattern | Type | Description | +|----------------------------------|:----:|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ^x- | Any | Allows extensions to the OpenAPI Schema. The field name MUST begin with `x-`, for example, `x-internal-id`. The value can be `null`, a primitive, an array or an object. Can have any valid JSON format value. | The extensions may or may not be supported by the available tooling, but those may be extended as well to add requested support (if tools are internal or open-sourced). @@ -4248,7 +4248,7 @@ Two examples of this: ## Appendix A: Revision History | Version | Date | Notes | -| ---------- | ---------- | ------------------------------------------------- | +|------------|------------|---------------------------------------------------| | 3.0.3-Pode | 2023-11-20 | OpenAPI Specification 3.0.3 Pode Version | | 3.0.3 | 2020-02-20 | Patch release of the OpenAPI Specification 3.0.3 | | 3.0.2 | 2018-10-08 | Patch release of the OpenAPI Specification 3.0.2 | diff --git a/docs/Tutorials/Routes/Overview.md b/docs/Tutorials/Routes/Overview.md index 9fbcae868..a9acd7efe 100644 --- a/docs/Tutorials/Routes/Overview.md +++ b/docs/Tutorials/Routes/Overview.md @@ -298,4 +298,4 @@ Static routes have a slightly different format: | OpenApi | hashtable[] | The OpenAPI definition/settings for the route | | Path | string | The path of the route - this path will have regex in place of dynamic file names | | Source | string | The source path within the server that is used for the route | -| TransferEncoding | string | The transfer encoding to use when parsing the payload in the request | +| TransferEncoding | string | The transfer encoding to use when parsing the payload in the request | \ No newline at end of file diff --git a/docs/Tutorials/Routes/Utilities/ContentTypes.md b/docs/Tutorials/Routes/Utilities/ContentTypes.md index 41800c84a..57816670b 100644 --- a/docs/Tutorials/Routes/Utilities/ContentTypes.md +++ b/docs/Tutorials/Routes/Utilities/ContentTypes.md @@ -21,6 +21,10 @@ Start-PodeServer { Add-PodeRoute -Method Get -Path '/api/xml' -ContentType 'text/xml' -ScriptBlock { Write-PodeXmlResponse -Value @{} } + + Add-PodeRoute -Method Get -Path '/api/yaml' -ContentType 'text/yaml' -ScriptBlock { + Write-PodeYamlResponse -Value @{} + } } ``` diff --git a/docs/Tutorials/Routes/Utilities/ErrorPages.md b/docs/Tutorials/Routes/Utilities/ErrorPages.md index ff2d05938..6801b265b 100644 --- a/docs/Tutorials/Routes/Utilities/ErrorPages.md +++ b/docs/Tutorials/Routes/Utilities/ErrorPages.md @@ -132,6 +132,9 @@ Above you'll see that the exception supplied to `status` will also be supplied t Once set to `true`, any available exception details for status codes will be available to error pages - a useful setting to have in a [`server.dev.psd1`](../../../Configuration#environments) file. +!!! important + This setting is not recommended for production environments due to the security risk associated with exposing internal exception details. + ### Content Types Using the `server.psd1` configuration file, you can define which file content types to attempt when generating error pages for routes. You can either: diff --git a/docs/Tutorials/Schedules.md b/docs/Tutorials/Schedules.md index 9c754bfb1..2665501d0 100644 --- a/docs/Tutorials/Schedules.md +++ b/docs/Tutorials/Schedules.md @@ -126,6 +126,29 @@ Get-PodeSchedule -Name Name1 Get-PodeSchedule -Name Name1, Name2 ``` +## Getting Schedule Processes + +You can retrieve a list of processes triggered by Schedules via [`Get-PodeScheduleProcess`](../../Functions/Schedules/Get-PodeScheduleProcess) - this will return processes created either by a Schedule's natural time-based trigger, or via [`Invoke-PodeSchedule`](../../Functions/Schedules/Invoke-PodeSchedule). + +You can either retrieve all processes, or filter them by Schedule Name, or Process ID/Status: + +```powershell +# retrieves all schedule processes +Get-PodeScheduleProcess + +# retrieves all schedule processes for the "ScheduleName" process +Get-PodeScheduleProcess -Name 'ScheduleName' + +# retrieves the schedule process with ID "ScheduleId" +Get-PodeScheduleProcess -Id 'ScheduleId' + +# retrieves all running schedule processes +Get-PodeScheduleProcess -State 'Running' + +# retrieves all pending schedule processes for "ScheduleName" +Get-PodeScheduleProcess -Name 'ScheduleName' -State 'Running' +``` + ## Next Trigger Time When you retrieve a Schedule using [`Get-PodeSchedule`](../../Functions/Schedules/Get-PodeSchedule), each Schedule object will already have its next trigger time as `NextTriggerTime`. However, if you want to get a trigger time further ino the future than this, then you can use the [`Get-PodeScheduleNextTrigger`](../../Functions/Schedules/Get-PodeScheduleNextTrigger) function. diff --git a/docs/Tutorials/Tasks.md b/docs/Tutorials/Tasks.md index ac257a98c..f6a98e339 100644 --- a/docs/Tutorials/Tasks.md +++ b/docs/Tutorials/Tasks.md @@ -51,7 +51,7 @@ Add-PodeTask -Name 'Example' -ArgumentList @{ Name = 'Rick'; Environment = 'Mult } ``` -Tasks parameters **must** be bound in the param block in order to be used, but the values for the paramters can be set through the `-ArgumentList` hashtable parameter in either the Add-PodeTask definition or when invoking the task. The following snippet would populate the parameters to the task with the same values as the above example but the `-ArgumentList` parameter is populated during invocation. Note that Keys in the `-ArgumentList` hashtable parameter set during invocation override the same Keys set during task creation: +Tasks parameters **must** be bound in the param block in order to be used, but the values for the paramters can be set through the `-ArgumentList` hashtable parameter in either the Add-PodeTask definition or when invoking the task. The following snippet would populate the parameters to the task with the same values as the above example but the `-ArgumentList` parameter is populated during invocation. Note that Keys in the `-ArgumentList` hashtable parameter set during invocation override the same Keys set during task creation: ```powershell Add-PodeTask -Name 'Example' -ScriptBlock { @@ -184,6 +184,29 @@ Get-PodeTask -Name Example1 Get-PodeTask -Name Example1, Example2 ``` +## Getting Task Processes + +You can retrieve a list of processes triggered by Tasks via [`Get-PodeTaskProcess`](../../Functions/Tasks/Get-PodeTaskProcess) - this will return processes created via [`Invoke-PodeTask`](../../Functions/Tasks/Invoke-PodeTask). + +You can either retrieve all processes, or filter them by Task Name, or Process ID/Status: + +```powershell +# retrieves all task processes +Get-PodeTaskProcess + +# retrieves all task processes for the "TaskName" process +Get-PodeTaskProcess -Name 'TaskName' + +# retrieves the task process with ID "TaskId" +Get-PodeTaskProcess -Id 'TaskId' + +# retrieves all running task processes +Get-PodeTaskProcess -State 'Running' + +# retrieves all pending task processes for "TaskName" +Get-PodeTaskProcess -Name 'TaskName' -State 'Running' +``` + ## Task Object !!! warning @@ -191,8 +214,8 @@ Get-PodeTask -Name Example1, Example2 The following is the structure of the Task object internally, as well as the object that is returned from [`Get-PodeTask`](../../Functions/Tasks/Get-PodeTask): -| Name | Type | Description | -| ---- | ---- | ----------- | -| Name | string | The name of the Task | -| Script | scriptblock | The scriptblock of the Task | -| Arguments | hashtable | The arguments supplied from ArgumentList | +| Name | Type | Description | +| --------- | ----------- | ---------------------------------------- | +| Name | string | The name of the Task | +| Script | scriptblock | The scriptblock of the Task | +| Arguments | hashtable | The arguments supplied from ArgumentList | diff --git a/docs/index.md b/docs/index.md index 533d2603b..25d0d9dd9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,8 +21,9 @@ Pode is a Cross-Platform framework to create web servers that host REST APIs, We * Cross-platform using PowerShell Core (with support for PS5) * Docker support, including images for ARM/Raspberry Pi * Azure Functions, AWS Lambda, and IIS support -* OpenAPI, Swagger, and ReDoc support -* Listen on a single or multiple IP address/hostnames +* OpenAPI specification version 3.0.x and 3.1.0 +* OpenAPI documentation with Swagger, Redoc, RapidDoc, StopLight, OpenAPI-Explorer and RapiPdf +* Listen on a single or multiple IP(v4/v6) addresses/hostnames * Cross-platform support for HTTP(S), WS(S), SSE, SMTP(S), and TCP(S) * Host REST APIs, Web Pages, and Static Content (with caching) * Support for custom error pages @@ -45,6 +46,8 @@ Pode is a Cross-Platform framework to create web servers that host REST APIs, We * Support for File Watchers * In-memory caching, with optional support for external providers (such as Redis) * (Windows) Open the hosted server as a desktop application +* FileBrowsing support +* Localization (i18n) in Arabic, German, Spanish, France, Italian, Japanese, Korean, Polish, Portuguese, and Chinese ## 🏢 Companies using Pode diff --git a/docs/release-notes.md b/docs/release-notes.md index c96072d0d..969c65a6d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,8 +1,70 @@ # Release Notes +## v2.11.0 + +Date: 29th September 2024 + +```plain +### Features +* #1320: Enhanced Internationalization Support (i18n) (thanks @mdaneri!) + +### Enhancements +* #1338: Automate Endpoint Assignment for OAViewer in Pode (thanks @mdaneri!) +* #1339: Add 'Rename-PodeOADefinitionTag' Function (thanks @mdaneri!) +* #1340: Add configuration parameter 'Web.OpenApi.UsePodeYamlInternal' (thanks @mdaneri!) +* #1352: Update MIME types to comply with RFC9512 and RFC5323 (thanks @mdaneri!) +* #1356: Dutch language support (thanks @mdaneri!) +* #1373: Dynamically load the Pode DLL relevant to the version of PowerShell (thanks @mdaneri!) +* #1384: Customizing and Managing Runspace Names (thanks @mdaneri!) +* #1388: Support passing Arrays to Functions Using Piping (thanks @mdaneri!) +* #1393: Adds functions for retrieving Schedule and Task Processes +* #1393: Improves Error Handling in Schedules, Timers, Tasks, and Logging +* #1393: Removes Global scope from TimerEvent +* #1399: Replaces occurrences of New-Object with new() + +### Bugs +* #1319: Fixes the Write-Pode(*)Response functions so the Value parameter appropriately handles when an array is passed using piping (thanks @mdaneri!) +* #1321: Fixes a misspelled variable in Add-PodeOAExternalRoute (thanks @mdaneri!) +* #1347: '-AdditionalProperties' doesn't appear on the OpenAPI document despite using the '-NoAdditionalProperties' parameter. (thanks @mdaneri!) +* #1358: Fixes [ordered] comparisons in PowerShell 5.1 (thanks @mdaneri!) +* #1358: Fixes for various OpenAPI issues (thanks @mdaneri!) +* #1358: Fixes OpenAPI version validation check in PowerShell 5.1 (thanks @mdaneri!) +* #1359: Fixes the login redirect URL logic for OAuth2 flows when using -SuccessUseOrigin +* #1360: Fixes a bug when exporting more than 1 module +* #1369: Accurate Output with -NoDefaultResponses (thanks @mdaneri!) +* #1369: Correct Schema with -NoProperties (thanks @mdaneri!) +* #1369: Fixes for OpenAPI Generation: Exception with oneOf/anyOf/allOf (thanks @mdaneri!) +* #1369: Include Min/Max Properties (thanks @mdaneri!) +* #1369: Prevent Request Body on GET Operations (thanks @mdaneri!) +* #1379: Fixes SSL timeouts when running Pode in PS7.4+ +* #1390: Changes "-ContentMediaType" and "-MediaType" parameters to "-ContentType" on most OpenAPI functions (thanks @mdaneri!) +* #1390: Ensures the generated OpenAPI document now maintains element ordering (thanks @mdaneri!) +* #1390: Fixes OpenAPI DefinitionTag being null in some functions (thanks @mdaneri!) +* #1390: Fixes OpenAPI PowerShell 5.1 compatibility issue while testing schemas (thanks @mdaneri!) +* #1397: Fixes retrieving DNS domain name on macOS +* #1400: Fixes session scoped variable when remapping while setting values +* #1400: Fixes User being needlessly splatted when passed to scriptblock for some Authentication methods + +### Documentation +* #1332: Adds documentation for CORS (thanks @mdaneri!) +* #1332: Adds missing features to the Feature List (thanks @mdaneri!) +* #1332: Splits OpenAPI documentation into multiple pages (thanks @mdaneri!) +* #1332: Updates Known Issues for PowerShell classes with PS7.4's SafeThread support (thanks @mdaneri!) +* #1333: Cleans up the Examples in the repository, and adds them to the Documentation (thanks @mdaneri!) + +### Packaging +* #1322: Applies a fix for a PS7.5 bug with Remove-Item throwing divide by zero error +* #1323: Fix build error when dotnet tries to restore from offline NuGet cache +* #1328: Make preview builds optional for PR merges +* #1342: Add GitHub Codespace Configuration and Getting Started Guide for Pode (thanks @mdaneri!) + +### Dependencies +* #1341: Bump actions/add-to-project from 1.0.1 to 1.0.2 +``` + ## v2.10.1 -Data: 27th May 2024 +Date: 27th May 2024 ```plain ### Bugs diff --git a/examples/Caching.ps1 b/examples/Caching.ps1 new file mode 100644 index 000000000..a0dff16ca --- /dev/null +++ b/examples/Caching.ps1 @@ -0,0 +1,104 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server and integrate with Redis for caching. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081 with Redis integration for caching. + It checks for the existence of the `redis-cli` command and sets up a Pode server with routes + that demonstrate caching with Redis. + +.EXAMPLE + To run the sample: ./Caching.ps1 + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Caching.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +try { + if (Get-Command redis-cli -ErrorAction Stop) { + Write-Output 'redis-cli exists.' + } +} +catch { + throw 'Cannot continue redis-cli does not exist.' +} + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 3 { + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + # log errors + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + Set-PodeCacheDefaultTtl -Value 60 + + $params = @{ + Set = { + param($key, $value, $ttl) + $null = redis-cli -h localhost -p 6379 SET $key "$($value)" EX $ttl + } + Get = { + param($key, $metadata) + $result = redis-cli -h localhost -p 6379 GET $key + $result = [System.Management.Automation.Internal.StringDecorated]::new($result).ToString('PlainText') + if ([string]::IsNullOrEmpty($result) -or ($result -ieq '(nil)')) { + return $null + } + return $result + } + Test = { + param($key) + $result = redis-cli -h localhost -p 6379 EXISTS $key + return [System.Management.Automation.Internal.StringDecorated]::new($result).ToString('PlainText') + } + Remove = { + param($key) + $null = redis-cli -h localhost -p 6379 EXPIRE $key -1 + } + Clear = {} + } + if ($params) { + Add-PodeCacheStorage -Name 'Redis' @params + + # set default value for cache + $cache:cpu = (Get-Random -Minimum 1 -Maximum 1000) + + # get cpu, and cache it + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + if ((Test-PodeCache -Key 'cpu') -and ($null -ne $cache:cpu)) { + Write-PodeJsonResponse -Value @{ CPU = $cache:cpu } + # Write-PodeHost 'here - cached' + return + } + + # $cache:cpu = (Get-Counter '\Processor(_Total)\% Processor Time').CounterSamples.CookedValue + Start-Sleep -Milliseconds 500 + $cache:cpu = (Get-Random -Minimum 1 -Maximum 1000) + Write-PodeJsonResponse -Value @{ CPU = $cache:cpu } + # $cpu = (Get-Random -Minimum 1 -Maximum 1000) + # Write-PodeJsonResponse -Value @{ CPU = $cpu } + # Write-PodeHost 'here - raw' + } + } + +} \ No newline at end of file diff --git a/examples/Create-Routes.ps1 b/examples/Create-Routes.ps1 new file mode 100644 index 000000000..05ebeb1c0 --- /dev/null +++ b/examples/Create-Routes.ps1 @@ -0,0 +1,58 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with multiple routes using different approaches. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081 with various routes demonstrating different + approaches to route creation. These include using script blocks, file paths, and direct script inclusion. + +.EXAMPLE + To run the sample: ./Create-Routes.ps1 + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Create-Routes.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +#crete routes using different approaches +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +Start-PodeServer -Threads 1 -ScriptBlock { + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + Add-PodeRoute -PassThru -Method Get -Path '/routeCreateScriptBlock/:id' -ScriptBlock ([ScriptBlock]::Create( (Get-Content -Path "$ScriptPath\scripts\routeScript.ps1" -Raw))) | + Set-PodeOARouteInfo -Summary 'Test' -OperationId 'routeCreateScriptBlock' -PassThru | + Set-PodeOARequest -Parameters @((New-PodeOAStringProperty -Name 'id' | ConvertTo-PodeOAParameter -In Path -Required) ) + + + Add-PodeRoute -PassThru -Method Post -Path '/routeFilePath/:id' -FilePath '.\scripts\routeScript.ps1' | Set-PodeOARouteInfo -Summary 'Test' -OperationId 'routeFilePath' -PassThru | + Set-PodeOARequest -Parameters @((New-PodeOAStringProperty -Name 'id' | ConvertTo-PodeOAParameter -In Path -Required) ) + + + Add-PodeRoute -PassThru -Method Get -Path '/routeScriptBlock/:id' -ScriptBlock { $Id = $WebEvent.Parameters['id'] ; Write-PodeJsonResponse -StatusCode 200 -Value @{'id' = $Id } } | + Set-PodeOARouteInfo -Summary 'Test' -OperationId 'routeScriptBlock' -PassThru | + Set-PodeOARequest -Parameters @((New-PodeOAStringProperty -Name 'id' | ConvertTo-PodeOAParameter -In Path -Required) ) + + + Add-PodeRoute -PassThru -Method Get -Path '/routeScriptSameScope/:id' -ScriptBlock { . $ScriptPath\scripts\routeScript.ps1 } | + Set-PodeOARouteInfo -Summary 'Test' -OperationId 'routeScriptSameScope' -PassThru | + Set-PodeOARequest -Parameters @((New-PodeOAStringProperty -Name 'id' | ConvertTo-PodeOAParameter -In Path -Required) ) + +} \ No newline at end of file diff --git a/examples/Dockerfile b/examples/Dockerfile index 65ff8c543..67cb0d221 100644 --- a/examples/Dockerfile +++ b/examples/Dockerfile @@ -1,4 +1,4 @@ FROM badgerati/pode:test COPY . /usr/src/app/ -EXPOSE 8085 -CMD [ "pwsh", "-c", "cd /usr/src/app; ./web-pages-docker.ps1" ] +EXPOSE 8081 +CMD [ "pwsh", "-c", "cd /usr/src/app; ./Web-PagesDocker.ps1" ] diff --git a/examples/Dot-SourceScript.ps1 b/examples/Dot-SourceScript.ps1 new file mode 100644 index 000000000..cd97e396f --- /dev/null +++ b/examples/Dot-SourceScript.ps1 @@ -0,0 +1,45 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server and run a script from an external file. + +.DESCRIPTION + This script sets up a Pode server, enables terminal logging for errors, and uses an external + script for additional logic. It imports the Pode module from the source path if available, + otherwise from the installed modules. + +.EXAMPLE + To run the sample: ./Dot-SourceScript.ps1 + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Dot-SourceScript.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# runs the logic once, then exits +Start-PodeServer { + + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + Use-PodeScript -Path './modules/Script1.ps1' + +} diff --git a/examples/External-Funcs.ps1 b/examples/External-Funcs.ps1 new file mode 100644 index 000000000..21ef57a82 --- /dev/null +++ b/examples/External-Funcs.ps1 @@ -0,0 +1,52 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server and use an external module function. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081, imports an external module containing functions, + and includes a route that uses a function from the external module to generate a response. + +.EXAMPLE + To run the sample: ./External-Funcs.ps1 + + Invoke-RestMethod -Uri http://localhost:8081 -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/External-Funcs.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} catch { throw } + +# or just: +# Import-Module Pode + +# include the external function module +Import-PodeModule -Path './modules/External-Funcs.psm1' + +# create a server, and start listening on port 8081 +Start-PodeServer { + + # listen on localhost:8085 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + # GET request for "localhost:8085/" + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Write-PodeJsonResponse -Value @{ 'result' = (Get-Greeting) } + } + +} \ No newline at end of file diff --git a/examples/File-Monitoring.ps1 b/examples/File-Monitoring.ps1 new file mode 100644 index 000000000..10be1b4ac --- /dev/null +++ b/examples/File-Monitoring.ps1 @@ -0,0 +1,51 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with a view engine and file monitoring. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081, uses Pode's view engine for rendering + web pages, and configures the server to monitor file changes and restart automatically. + +.EXAMPLE + To run the sample: ./File-Monitoring.ps1 + + Invoke-RestMethod -Uri http://localhost:8081 -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/File-Monitoring.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server listening on port 8081, set to monitor file changes and restart the server +Start-PodeServer { + + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + Set-PodeViewEngine -Type Pode + + # GET request for web page on "localhost:8081/" + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(1, 2, 3); } + } + +} diff --git a/examples/File-Watchers.ps1 b/examples/File-Watchers.ps1 new file mode 100644 index 000000000..eac1ec9ce --- /dev/null +++ b/examples/File-Watchers.ps1 @@ -0,0 +1,47 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with file watcher and logging. + +.DESCRIPTION + This script sets up a Pode server, enables terminal logging for errors, and adds a file watcher + to monitor changes in PowerShell script files (*.ps1) within the script directory. The server + logs file change events and outputs them to the terminal. + +.EXAMPLE + To run the sample: ./File-Watchers.ps1 + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/File-Watchers.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +Start-PodeServer -Verbose { + + # enable logging + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + Add-PodeFileWatcher -Path $ScriptPath -Include '*.ps1' -ScriptBlock { + "[$($FileEvent.Type)][$($FileEvent.Parameters['project'])]: $($FileEvent.FullPath)" | Out-Default + } +} \ No newline at end of file diff --git a/examples/FileBrowser/FileBrowser.ps1 b/examples/FileBrowser/FileBrowser.ps1 index ab94f1977..da79d9a40 100644 --- a/examples/FileBrowser/FileBrowser.ps1 +++ b/examples/FileBrowser/FileBrowser.ps1 @@ -1,11 +1,43 @@ -$FileBrowserPath = Split-Path -Parent -Path $MyInvocation.MyCommand.Path -$podePath = Split-Path -Parent -Path (Split-Path -Parent -Path $FileBrowserPath) -if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { - Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop -} -else { - Import-Module -Name 'Pode' +<# +.SYNOPSIS + PowerShell script to set up a Pode server with static file browsing and authentication. + +.DESCRIPTION + This script sets up a Pode server that listens on port 8081. It includes static file browsing + with different routes, some of which require authentication. The script also demonstrates + how to set up basic authentication using Pode. + + The server includes routes for downloading files, browsing files without downloading, and + accessing files with authentication. + +.EXAMPLE + To run the sample: ./FileBrowser/FileBrowser.ps1 + + Access the file browser: + Navigate to 'http://localhost:8081/' to browse the files in the specified directory. + Download a file: + Navigate to 'http://localhost:8081/download' to download files. + Access a file with authentication: + Navigate to 'http://localhost:8081/auth' and provide the username 'morty' and password 'pickle'. + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/FileBrowser/FileBrowser.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + $FileBrowserPath = Split-Path -Parent -Path $MyInvocation.MyCommand.Path + $podePath = Split-Path -Parent -Path (Split-Path -Parent -Path $FileBrowserPath) + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } } +catch { throw } $directoryPath = $podePath # Start Pode server diff --git a/examples/HelloWorld/HelloWorld.ps1 b/examples/HelloWorld/HelloWorld.ps1 new file mode 100644 index 000000000..949e00c22 --- /dev/null +++ b/examples/HelloWorld/HelloWorld.ps1 @@ -0,0 +1,56 @@ +<# +.SYNOPSIS + PowerShell script to set up a Pode server with a simple GET endpoint. + +.DESCRIPTION + This script sets up a Pode server that listens on port 8080. It includes a single route for GET requests + to the root path ('/') that returns a simple text response. + +.EXAMPLE + To run the sample: ./HelloWorld/HelloWorld.ps1 + + # HTML responses 'Hello, world! + Invoke-RestMethod -Uri http://localhost:8081/ -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/HelloWorld/HelloWorld.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Get the path of the script being executed + $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)) + # Get the parent directory of the script's path + $podePath = Split-Path -Parent -Path $ScriptPath + + # Check if the Pode module file exists in the specified path + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + # If the Pode module file exists, import it + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + # If the Pode module file does not exist, import the Pode module from the system + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { + # If there is any error during the module import, throw the error + throw +} + +# Alternatively, you can directly import the Pode module from the system +# Import-Module Pode + +# Start the Pode server +Start-PodeServer { + # Add an HTTP endpoint listening on localhost at port 8080 + Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http + + # Add a route for GET requests to the root path '/' + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + # Send a text response with 'Hello, world!' + Write-PodeTextResponse -Value 'Hello, world!' + } +} diff --git a/examples/IIS-Example.ps1 b/examples/IIS-Example.ps1 new file mode 100644 index 000000000..11afa61b5 --- /dev/null +++ b/examples/IIS-Example.ps1 @@ -0,0 +1,67 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with logging and task scheduling. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081, enables terminal logging for requests and errors, + and includes a scheduled task. The server has two routes: one for a simple JSON response and another to + invoke a task that demonstrates delayed execution. + +.EXAMPLE + To run the sample: ./IIS-Example.ps1 + + + Invoke-RestMethod -Uri http://localhost:8081 -Method Get + + Invoke-RestMethod -Uri http://localhost:8081/run-task -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/IIS-Example.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + Add-PodeTask -Name 'Test' -ScriptBlock { + Start-Sleep -Seconds 10 + 'a message is never late, it arrives exactly when it means to' | Out-Default + } + + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Write-PodeJsonResponse -Value @{ Message = 'Hello' } + $WebEvent.Request | out-default + } + + Add-PodeRoute -Method Get -Path '/run-task' -ScriptBlock { + Invoke-PodeTask -Name 'Test' | Out-Null + Write-PodeJsonResponse -Value @{ Result = 'jobs done' } + } + +} \ No newline at end of file diff --git a/examples/Logging.ps1 b/examples/Logging.ps1 new file mode 100644 index 000000000..2625991bf --- /dev/null +++ b/examples/Logging.ps1 @@ -0,0 +1,85 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with configurable logging, view engine, and various routes. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081, configures a view engine, and allows for different + types of request logging (terminal, file, custom). It includes routes for serving a web page, simulating a + server error, and downloading a file. + +.EXAMPLE + To run the sample: ./Logging.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/ -Method Get + Invoke-RestMethod -Uri http://localhost:8081/error -Method Get + Invoke-RestMethod -Uri http://localhost:8081/download -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Logging.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +$LOGGING_TYPE = 'terminal' # Terminal, File, Custom + +# create a server, and start listening on port 8081 +Start-PodeServer { + + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + Set-PodeViewEngine -Type Pode + + switch ($LOGGING_TYPE.ToLowerInvariant()) { + 'terminal' { + New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging + } + + 'file' { + New-PodeLoggingMethod -File -Name 'requests' -MaxDays 4 | Enable-PodeRequestLogging + } + + 'custom' { + $type = New-PodeLoggingMethod -Custom -ScriptBlock { + param($item) + # send request row to S3 + } + + $type | Enable-PodeRequestLogging + } + } + + # GET request for web page on "localhost:8081/" + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(1, 2, 3); } + } + + # GET request throws fake "500" server error status code + Add-PodeRoute -Method Get -Path '/error' -ScriptBlock { + Set-PodeResponseStatus -Code 500 + } + + # GET request to download a file + Add-PodeRoute -Method Get -Path '/download' -ScriptBlock { + Set-PodeResponseAttachment -Path 'Anger.jpg' + } + +} diff --git a/examples/Looping-Service.ps1 b/examples/Looping-Service.ps1 new file mode 100644 index 000000000..22b70e8bd --- /dev/null +++ b/examples/Looping-Service.ps1 @@ -0,0 +1,53 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with interval-based service handlers. + +.DESCRIPTION + This script sets up a Pode server that runs with a specified interval, adding service handlers + that execute at each interval. The handlers include logging messages to the terminal and using + lock mechanisms. + +.EXAMPLE + To run the sample: ./Looping-Service.ps1 + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Looping-Service.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start looping +Start-PodeServer -Interval 3 { + + Add-PodeHandler -Type Service -Name 'Hello' -ScriptBlock { + Write-PodeHost 'hello, world!' + Lock-PodeObject -ScriptBlock { + "Look I'm locked!" | Out-PodeHost + } + } + + Add-PodeHandler -Type Service -Name 'Bye' -ScriptBlock { + Write-PodeHost 'goodbye!' + } + +} diff --git a/examples/Mail-Server.ps1 b/examples/Mail-Server.ps1 new file mode 100644 index 000000000..e3bc9ce41 --- /dev/null +++ b/examples/Mail-Server.ps1 @@ -0,0 +1,80 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with SMTP and SMTPS protocols. + +.DESCRIPTION + This script sets up a Pode server listening on SMTP (port 25) and SMTPS (with explicit and implicit TLS). + It includes logging for errors and debug information and demonstrates handling incoming SMTP emails with + potential attachments. + +.EXAMPLE + To run the sample: ./Mail-Server.ps1 + + Send-MailMessage -SmtpServer localhost -To 'to@pode.com' -From 'from@pode.com' -Body 'Hello' -Subject 'Hi there' -Port 25 + +.EXAMPLE + To run the sample: ./Mail-Server.ps1 + [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { return $true } + Send-MailMessage -SmtpServer localhost -To 'to@pode.com' -From 'from@pode.com' -Body 'Hello' -Subject 'Hi there' -Port 587 -UseSSL + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Mail-Server.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 25 +Start-PodeServer -Threads 2 { + + Add-PodeEndpoint -Address localhost -Protocol Smtp + Add-PodeEndpoint -Address localhost -Protocol Smtps -SelfSigned -TlsMode Explicit + Add-PodeEndpoint -Address localhost -Protocol Smtps -SelfSigned -TlsMode Implicit + + # enable logging + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging -Levels Error, Debug + + # allow the local ip + #Add-PodeAccessRule -Access Allow -Type IP -Values 127.0.0.1 + + # setup an smtp handler + Add-PodeHandler -Type Smtp -Name 'Main' -ScriptBlock { + Write-PodeHost '- - - - - - - - - - - - - - - - - -' + Write-PodeHost $SmtpEvent.Email.From + Write-PodeHost $SmtpEvent.Email.To + Write-PodeHost '|' + Write-PodeHost $SmtpEvent.Email.Body + Write-PodeHost '|' + # Write-PodeHost $SmtpEvent.Email.Data + # Write-PodeHost '|' + $SmtpEvent.Email.Attachments | Out-Default + if ($SmtpEvent.Email.Attachments.Length -gt 0) { + #$SmtpEvent.Email.Attachments[0].Save('C:\temp') + } + Write-PodeHost '|' + $SmtpEvent.Email | Out-Default + $SmtpEvent.Request | out-default + $SmtpEvent.Email.Headers | out-default + Write-PodeHost '- - - - - - - - - - - - - - - - - -' + } + +} \ No newline at end of file diff --git a/examples/middleware.ps1 b/examples/Middleware.ps1 similarity index 61% rename from examples/middleware.ps1 rename to examples/Middleware.ps1 index 575d93678..01e489e32 100644 --- a/examples/middleware.ps1 +++ b/examples/Middleware.ps1 @@ -1,13 +1,49 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with rate limiting and middleware. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081 with various middleware implementations + and rate limiting for incoming requests. It includes middleware for route-specific logic, blocking + specific user agents, and rejecting requests from certain IP addresses. + +.EXAMPLE + To run the sample: ./Middleware.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/users -Method Get + Invoke-RestMethod -Uri http://localhost:8081/alive -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Middleware.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + # or just: # Import-Module Pode Start-PodeServer { - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port $port -Protocol Http + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http # limit localhost to 5 request per 10 seconds Add-PodeLimitRule -Type IP -Values @('127.0.0.1', '[::1]') -Limit 5 -Seconds 10 diff --git a/examples/OneOff-Script.ps1 b/examples/OneOff-Script.ps1 new file mode 100644 index 000000000..e3547babf --- /dev/null +++ b/examples/OneOff-Script.ps1 @@ -0,0 +1,44 @@ +<# +.SYNOPSIS + A simple PowerShell script to set up a Pode server and log a message. + +.DESCRIPTION + This script sets up a Pode server, enables terminal logging for errors, and writes a "hello, world!" message to the terminal. + The server runs the logic once and then exits. + +.EXAMPLE + To run the sample: ./OneOff-Script.ps1 + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/OneOff-Script.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# runs the logic once, then exits +Start-PodeServer { + + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + Write-PodeHost 'hello, world!' + +} diff --git a/examples/OpenApiTuttiFrutti.ps1 b/examples/OpenApi-TuttiFrutti.ps1 similarity index 86% rename from examples/OpenApiTuttiFrutti.ps1 rename to examples/OpenApi-TuttiFrutti.ps1 index 691c1043b..358906a22 100644 --- a/examples/OpenApiTuttiFrutti.ps1 +++ b/examples/OpenApi-TuttiFrutti.ps1 @@ -1,12 +1,74 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -if (Test-Path -Path "$($path)/src/Pode.psm1" -PathType Leaf) { - Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop -} else { - Import-Module -Name 'Pode' +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with OpenAPI 3.0 and 3.1 specifications. + +.DESCRIPTION + This script sets up a Pode server listening on ports 8081 and 8082 with OpenAPI 3.0 and 3.1 specifications. + It includes multiple endpoints, OpenAPI documentation, various route definitions, authentication schemes, + and middleware for enhanced API functionality. + +.PARAMETER PortV3 + The port on which the Pode server will listen for OpenAPI v3. Default is 8080. + +.PARAMETER PortV3_1 + The port on which the Pode server will listen for OpenAPI v3_1. Default is 8081. + +.PARAMETER Quiet + Suppresses output when the server is running. + +.PARAMETER DisableTermination + Prevents the server from being terminated. + +.EXAMPLE + To run the sample: ./OpenApi-TuttiFrutti.ps1 + + Using a browser to access the OpenAPI Info: + 'v3.1': http://127.0.0.1:8082/docs/openapi/v3.1 + Documentation: http://127.0.0.1:8082/docs/v3.1 + + 'v3': http://127.0.0.1:8081/docs/openapi/v3.0 + Documentation: http://127.0.0.1:8081/docs + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/OpenApi-TuttiFrutti.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +param( + [Parameter()] + [int] + $PortV3 = 8080, + + [int] + $PortV3_1 = 8081, + + [switch] + $Quiet, + + [switch] + $DisableTermination +) + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } } +catch { throw } -Start-PodeServer -Threads 2 -ScriptBlock { - Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http -Default +Start-PodeServer -Threads 1 -Quiet:$Quiet -DisableTermination:$DisableTermination -ScriptBlock { + Add-PodeEndpoint -Address localhost -Port $PortV3 -Protocol Http -Default -Name 'endpoint_v3' + Add-PodeEndpoint -Address localhost -Port $PortV3_1 -Protocol Http -Default -Name 'endpoint_v3.1' New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging $InfoDescription = @' This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about Swagger at [http://swagger.io](http://swagger.io). @@ -19,14 +81,8 @@ Some useful links: - [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml) '@ - - - #Enable-PodeOpenApi -Path '/docs/openapi' -OpenApiVersion '3.0.0' -EnableSchemaValidation -DisableMinimalDefinitions -DefaultResponses @{} - # New-PodeOAExternalDoc -Name 'SwaggerDocs' -Description 'Find out more about Swagger' -Url 'http://swagger.io' - # Add-PodeOAExternalDoc -Reference 'SwaggerDocs' - - Enable-PodeOpenApi -Path '/docs/openapi/v3.0' -OpenApiVersion '3.0.3' -EnableSchemaValidation -DisableMinimalDefinitions -NoDefaultResponses -DefinitionTag 'v3' - Enable-PodeOpenApi -Path '/docs/openapi/v3.1' -OpenApiVersion '3.1.0' -EnableSchemaValidation -DisableMinimalDefinitions -NoDefaultResponses -DefinitionTag 'v3.1' + Enable-PodeOpenApi -Path '/docs/openapi/v3.0' -OpenApiVersion '3.0.3' -EnableSchemaValidation:($PSVersionTable.PSEdition -eq 'Core') -DisableMinimalDefinitions -NoDefaultResponses -DefinitionTag 'v3' -EndpointName 'endpoint_v3' + Enable-PodeOpenApi -Path '/docs/openapi/v3.1' -OpenApiVersion '3.1.0' -EnableSchemaValidation:($PSVersionTable.PSEdition -eq 'Core') -DisableMinimalDefinitions -NoDefaultResponses -DefinitionTag 'v3.1' -EndpointName 'endpoint_v3.1' $swaggerDocs = New-PodeOAExternalDoc -Description 'Find out more about Swagger' -Url 'http://swagger.io' $swaggerDocs | Add-PodeOAExternalDoc -DefinitionTag 'v3', 'v3.1' @@ -201,7 +257,7 @@ Some useful links: (New-PodeOAStringProperty -Name 'photoUrls' -Array), (New-PodeOASchemaProperty -Name 'tags' -Component 'Tag') (New-PodeOAStringProperty -Name 'status' -Description 'pet status in the store' -Enum @('available', 'pending', 'sold')) - )) #> + )) #> <# Add-PodeOAComponentSchema -Name 'Cat' -Schema ( New-PodeOAObjectProperty -Name 'testcat' -Description 'Type of cat' -Properties ( New-PodeOAStringProperty -Name 'breed' -Description 'Type of Breed' -Enum @( 'Abyssinian', 'Balinese-Javanese', 'Burmese', 'British Shorthair') | @@ -295,7 +351,8 @@ Some useful links: Code = 401 Challenge = 'qop="auth", nonce=""' } - } else { + } + else { return @{ Message = 'No Authorization header found' Code = 401 @@ -329,7 +386,6 @@ Some useful links: $clientId = '123123123' $clientSecret = 'acascascasca>zzzcz' - $tenantId = '56456232' <# $InnerScheme = New-PodeAuthScheme -Form $scheme = New-PodeAuthScheme ` @@ -360,10 +416,10 @@ Some useful links: Merge-PodeAuth -Name 'test' -Authentication 'Login-OAuth2', 'api_key' $ex = - New-PodeOAExample -MediaType 'application/json' -Name 'user' -Summary 'User Example' -ExternalValue 'http://foo.bar/examples/user-example.json' | - New-PodeOAExample -MediaType 'application/xml' -Name 'user' -Summary 'User Example in XML' -ExternalValue 'http://foo.bar/examples/user-example.xml' | - New-PodeOAExample -MediaType 'text/plain' -Name 'user' -Summary 'User Example in Plain text' -ExternalValue 'http://foo.bar/examples/user-example.txt' | - New-PodeOAExample -MediaType '*/*' -Name 'user' -Summary 'User example in other forma' -ExternalValue 'http://foo.bar/examples/user-example.whatever' + New-PodeOAExample -ContentType 'application/json' -Name 'user' -Summary 'User Example' -ExternalValue 'http://foo.bar/examples/user-example.json' | + New-PodeOAExample -ContentType 'application/xml' -Name 'user' -Summary 'User Example in XML' -ExternalValue 'http://foo.bar/examples/user-example.xml' | + New-PodeOAExample -ContentType 'text/plain' -Name 'user' -Summary 'User Example in Plain text' -ExternalValue 'http://foo.bar/examples/user-example.txt' | + New-PodeOAExample -ContentType '*/*' -Name 'user' -Summary 'User example in other forma' -ExternalValue 'http://foo.bar/examples/user-example.whatever' Select-PodeOADefinition -Tag 'v3' -Scriptblock { Add-PodeRouteGroup -Path '/api/v4' -Routes { @@ -371,7 +427,8 @@ Some useful links: $JsonPet = ConvertTo-Json $WebEvent.data if ( Update-Pet -Id $WebEvent.Parameters['petId'] -Data $JsonPet) { Write-PodeJsonResponse -Value @{} -StatusCode 200 - } else { + } + else { Write-PodeJsonResponse -Value @{} -StatusCode 405 } } | Set-PodeOARouteInfo -Summary 'Updates a pet in the store with form data' -Tags 'pet' -OperationId 'updatePasdadaetWithForm' -PassThru | @@ -381,14 +438,15 @@ Some useful links: New-PodeOARequestBody -Description 'user to add to the system' -Content @{ 'application/json' = 'User'; 'application/xml' = 'User' } -Examples $ex ) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Pet updated.' -Content (@{ 'application/json' = '' ; 'application/xml' = '' }) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Pet updated.' -Content (@{ 'application/json' = '' ; 'application/xml' = '' }) -PassThru | Add-PodeOAResponse -StatusCode 405 -Description 'Method Not Allowed' -Content (@{ 'application/json' = '' ; 'application/xml' = '' }) Add-PodeRoute -PassThru -Method Put -Path '/paet/:petId' -ScriptBlock { $JsonPet = ConvertTo-Json $WebEvent.data if ( Update-Pet -Id $WebEvent.Parameters['id'] -Data $JsonPet) { Write-PodeJsonResponse -Value @{} -StatusCode 200 - } else { + } + else { Write-PodeJsonResponse -Value @{} -StatusCode 405 } } | Set-PodeOARouteInfo -Summary 'Updates a pet in the store with form data' -Tags 'pet' -OperationId 'updatepaet' -PassThru | @@ -416,7 +474,7 @@ Some useful links: Set-PodeOARequest -PassThru -Parameters ( New-PodeOAIntProperty -Name 'petId' -Description 'ID of pet to return' -Format Int64 | ConvertTo-PodeOAParameter -In Path -Required ) | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Pet') -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet') -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' -PassThru | Add-PodeOAResponse -StatusCode 404 -Description 'Pet not found' -PassThru | Add-PodeOAResponse -StatusCode 415 @@ -432,7 +490,8 @@ Some useful links: $JsonPet = ConvertTo-Json $WebEvent.data if ( Update-Pet -Id $WebEvent.Parameters['id'] -Data $JsonPet) { Write-PodeJsonResponse -Value @{} -StatusCode 200 - } else { + } + else { Write-PodeJsonResponse -Value @{} -StatusCode 405 } } | Set-PodeOARouteInfo -Summary 'Updates a pet in the store with form data' -Tags 'pet' -OperationId 'updatepaet2' -PassThru | @@ -445,10 +504,10 @@ Some useful links: $ex = - New-PodeOAExample -MediaType 'application/json' -Name 'user' -Summary 'User Example' -ExternalValue 'http://foo.bar/examples/user-example.json' | - New-PodeOAExample -MediaType 'application/xml' -Name 'user' -Summary 'User Example in XML' -ExternalValue 'http://foo.bar/examples/user-example.xml' | - New-PodeOAExample -MediaType 'text/plain' -Name 'user' -Summary 'User Example in Plain text' -ExternalValue 'http://foo.bar/examples/user-example.txt' | - New-PodeOAExample -MediaType '*/*' -Name 'user' -Summary 'User example in other forma' -ExternalValue 'http://foo.bar/examples/user-example.whatever' + New-PodeOAExample -ContentType 'application/json' -Name 'user' -Summary 'User Example' -ExternalValue 'http://foo.bar/examples/user-example.json' | + New-PodeOAExample -ContentType 'application/xml' -Name 'user' -Summary 'User Example in XML' -ExternalValue 'http://foo.bar/examples/user-example.xml' | + New-PodeOAExample -ContentType 'text/plain' -Name 'user' -Summary 'User Example in Plain text' -ExternalValue 'http://foo.bar/examples/user-example.txt' | + New-PodeOAExample -ContentType '*/*' -Name 'user' -Summary 'User example in other forma' -ExternalValue 'http://foo.bar/examples/user-example.whatever' Add-PodeOAComponentExample -name 'frog-example' -Summary "An example of a frog with a cat's name" -Value @{name = 'Jaguar'; petType = 'Panthera'; color = 'Lion'; gender = 'Male'; breed = 'Mantella Baroni' } @@ -456,16 +515,17 @@ Some useful links: $JsonPet = ConvertTo-Json $WebEvent.data if ( Update-Pet -Id $WebEvent.Parameters['id'] -Data $JsonPet) { Write-PodeJsonResponse -Value @{} -StatusCode 200 - } else { + } + else { Write-PodeJsonResponse -Value @{} -StatusCode 405 } } | Set-PodeOARouteInfo -Summary 'Updates a pet in the store with form data' -Tags 'pet' -OperationId 'updatepaet3' -PassThru | Set-PodeOARequest -Parameters @( (New-PodeOAStringProperty -Name 'petId' -Description 'ID of pet that needs to be updated' | ConvertTo-PodeOAParameter -In Path -Required) ) -RequestBody (New-PodeOARequestBody -Description 'user to add to the system' -Content @{ 'application/json' = 'NewCat' } -Examples ( - New-PodeOAExample -MediaType 'application/json' -Name 'cat' -Summary 'An example of a cat' -Value @{name = 'Fluffy'; petType = 'Cat'; color = 'White'; gender = 'male'; breed = 'Persian' } | - New-PodeOAExample -MediaType 'application/json' -Name 'dog' -Summary "An example of a dog with a cat's name" -Value @{name = 'Puma'; petType = 'Dog'; color = 'Black'; gender = 'Female'; breed = 'Mixed' } | - New-PodeOAExample -MediaType 'application/json' -Reference 'frog-example' + New-PodeOAExample -ContentType 'application/json' -Name 'cat' -Summary 'An example of a cat' -Value @{name = 'Fluffy'; petType = 'Cat'; color = 'White'; gender = 'male'; breed = 'Persian' } | + New-PodeOAExample -ContentType 'application/json' -Name 'dog' -Summary "An example of a dog with a cat's name" -Value @{name = 'Puma'; petType = 'Dog'; color = 'Black'; gender = 'Female'; breed = 'Mixed' } | + New-PodeOAExample -ContentType 'application/json' -Reference 'frog-example' ) ) -PassThru | Add-PodeOAResponse -StatusCode 200 -Description 'Pet updated.' -Content (@{ 'application/json' = '' ; 'application/xml' = '' }) -PassThru | @@ -476,26 +536,33 @@ Some useful links: $JsonPet = ConvertTo-Json $WebEvent.data if ( Update-Pet -Id $WebEvent.Parameters['id'] -Data $JsonPet) { Write-PodeJsonResponse -Value @{} -StatusCode 200 - } else { + } + else { Write-PodeJsonResponse -Value @{} -StatusCode 405 } } | Set-PodeOARouteInfo -Summary 'Updates a pet in the store with form data' -Tags 'pet' -OperationId 'updatepaet4' -PassThru | Set-PodeOARequest -Parameters @( (New-PodeOAStringProperty -Name 'petId' -Description 'ID of pet that needs to be updated' | ConvertTo-PodeOAParameter -In Path -Required -ContentType 'application/json') ) -RequestBody (New-PodeOARequestBody -Description 'user to add to the system' -Content @{ 'application/json' = 'Pet' } -Examples ( - New-PodeOAExample -MediaType 'application/json' -Name 'cat' -Summary 'An example of a cat' -Value @{name = 'Fluffy'; petType = 'Cat'; color = 'White'; gender = 'male'; breed = 'Persian' } | - New-PodeOAExample -MediaType 'application/json' -Name 'dog' -Summary "An example of a dog with a cat's name" -Value @{name = 'Puma'; petType = 'Dog'; color = 'Black'; gender = 'Female'; breed = 'Mixed' } | - New-PodeOAExample -MediaType 'application/json' -Reference 'frog-example' + New-PodeOAExample -ContentType 'application/json' -Name 'cat' -Summary 'An example of a cat' -Value @{name = 'Fluffy'; petType = 'Cat'; color = 'White'; gender = 'male'; breed = 'Persian' } | + New-PodeOAExample -ContentType 'application/json' -Name 'dog' -Summary "An example of a dog with a cat's name" -Value @{name = 'Puma'; petType = 'Dog'; color = 'Black'; gender = 'Female'; breed = 'Mixed' } | + New-PodeOAExample -ContentType 'application/json' -Reference 'frog-example' ) ) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Pet updated.' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content '') -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Pet updated.' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content '') -PassThru | Add-PodeOAResponse -StatusCode 405 -Description 'Method Not Allowed' -Content (@{ 'application/json' = '' ; 'application/xml' = '' }) } } Add-PodeAuthMiddleware -Name test -Authentication 'test' -Route '/api/*' Select-PodeOADefinition -Tag 'v3.1', 'v3' -Scriptblock { + + Add-PodeRoute -Method 'Post' -Path '/close' -ScriptBlock { + Close-PodeServer + } -PassThru | Set-PodeOARouteInfo -Summary 'Shutdown the server' -PassThru | Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' + + Add-PodeRouteGroup -Path '/api/v3' -Routes { #PUT Add-PodeRoute -PassThru -Method Put -Path '/pet' -ScriptBlock { @@ -505,7 +572,8 @@ Some useful links: $Pet = $WebEvent.data $Pet.tags.id = Get-Random -Minimum 1 -Maximum 9999999 Write-PodeJsonResponse -Value ($Pet | ConvertTo-Json -Depth 20 ) -StatusCode 200 - } else { + } + else { Write-PodeJsonResponse -StatusCode 405 -Value @{ result = $Validate.result message = $Validate.message -join ', ' @@ -513,11 +581,11 @@ Some useful links: } } | Set-PodeOARouteInfo -Summary 'Update an existing pet' -Description 'Update an existing pet by Id' -Tags 'pet' -OperationId 'updatePet' -PassThru | Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Reference 'PetBodySchema' ) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Pet' ) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet' ) -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' -PassThru | Add-PodeOAResponse -StatusCode 404 -Description 'Pet not found' -PassThru | Add-PodeOAResponse -StatusCode 405 -Description 'Validation exception' -Content @{ - 'application/json' = (New-PodeOAObjectProperty -Properties @( (New-PodeOAStringProperty -Name 'result'), (New-PodeOAStringProperty -Name 'message') )) + 'application/json' = (New-PodeOAObjectProperty -Properties @((New-PodeOAStringProperty -Name 'result'), (New-PodeOAStringProperty -Name 'message'))) } Add-PodeRoute -PassThru -Method Post -Path '/pet' -Authentication 'Login-OAuth2' -Scope 'write' -ScriptBlock { @@ -528,7 +596,8 @@ Some useful links: $Pet = $WebEvent.data $Pet.tags.id = Get-Random -Minimum 1 -Maximum 9999999 Write-PodeJsonResponse -Value ($Pet | ConvertTo-Json -Depth 20 ) -StatusCode 200 - } else { + } + else { Write-PodeJsonResponse -StatusCode 405 -Value @{ result = $Validate.result message = $Validate.message -join ', ' @@ -536,9 +605,9 @@ Some useful links: } } | Set-PodeOARouteInfo -Summary 'Add a new pet to the store' -Description 'Add a new pet to the store' -Tags 'pet' -OperationId 'addPet' -PassThru | Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Reference 'PetBodySchema' ) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Pet' ) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet' ) -PassThru | Add-PodeOAResponse -StatusCode 405 -Description 'Validation exception' -Content @{ - 'application/json' = (New-PodeOAObjectProperty -Properties @( (New-PodeOAStringProperty -Name 'result'), (New-PodeOAStringProperty -Name 'message') )) + 'application/json' = (New-PodeOAObjectProperty -Properties @((New-PodeOAStringProperty -Name 'result'), (New-PodeOAStringProperty -Name 'message'))) } Add-PodeRoute -PassThru -Method Post -Path '/petcallback' -Authentication 'Login-OAuth2' -Scope 'write' -ScriptBlock { @@ -548,7 +617,8 @@ Some useful links: $Pet = $WebEvent.data $Pet.tags.id = Get-Random -Minimum 1 -Maximum 9999999 Write-PodeJsonResponse -Value ($Pet | ConvertTo-Json -Depth 20 ) -StatusCode 200 - } else { + } + else { Write-PodeJsonResponse -StatusCode 405 -Value @{ result = $Validate.result message = $Validate.message -join ', ' @@ -556,13 +626,13 @@ Some useful links: } } | Set-PodeOARouteInfo -Summary 'Add a new pet to the store' -Description 'Add a new pet to the store' -Tags 'pet' -OperationId 'addPetcallback' -PassThru | Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Reference 'PetBodySchema' ) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Pet' ) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet' ) -PassThru | Add-PodeOAResponse -StatusCode 405 -Description 'Validation exception' -Content @{ - 'application/json' = (New-PodeOAObjectProperty -Properties @( (New-PodeOAStringProperty -Name 'result'), (New-PodeOAStringProperty -Name 'message') )) + 'application/json' = (New-PodeOAObjectProperty -Properties @((New-PodeOAStringProperty -Name 'result'), (New-PodeOAStringProperty -Name 'message'))) } -PassThru | Add-PodeOACallBack -Name 'test' -Path '{$request.body#/id}' -Method Post -RequestBody (New-PodeOARequestBody -Content @{'*/*' = (New-PodeOAStringProperty -Name 'id') } ) ` -Response ( - New-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Pet' -Array) | + New-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet' -Array) | New-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' | New-PodeOAResponse -StatusCode 404 -Description 'Pet not found' | New-PodeOAResponse -Default -Description 'Something is wrong' @@ -572,7 +642,7 @@ Some useful links: Add-PodeOAComponentCallBack -Name 'test' -Path '{$request.body#/id}' -Method Post -RequestBody (New-PodeOARequestBody -Content @{'*/*' = (New-PodeOAStringProperty -Name 'id') } ) ` -Response ( - New-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Pet' -Array) | + New-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet' -Array) | New-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' | New-PodeOAResponse -StatusCode 404 -Description 'Pet not found' | New-PodeOAResponse -Default -Description 'Something is wrong' @@ -586,7 +656,8 @@ Some useful links: $Pet = $WebEvent.data $Pet.tags.id = Get-Random -Minimum 1 -Maximum 9999999 Write-PodeJsonResponse -Value ($Pet | ConvertTo-Json -Depth 20 ) -StatusCode 200 - } else { + } + else { Write-PodeJsonResponse -StatusCode 405 -Value @{ result = $Validate.result message = $Validate.message -join ', ' @@ -594,7 +665,7 @@ Some useful links: } } | Set-PodeOARouteInfo -Summary 'Add a new pet to the store' -Description 'Add a new pet to the store' -Tags 'pet' -OperationId 'petcallbackReference' -PassThru | Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Reference 'PetBodySchema' ) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Pet' ) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet' ) -PassThru | Add-PodeOAResponse -StatusCode 405 -Description 'Validation exception' -Content @{ 'application/json' = ( New-PodeOAStringProperty -Name 'result' | New-PodeOAStringProperty -Name 'message' | New-PodeOAObjectProperty ) } -PassThru | @@ -607,30 +678,22 @@ Some useful links: Set-PodeOARequest -PassThru -Parameters @( ( New-PodeOAStringProperty -Name 'status' -Description 'Status values that need to be considered for filter' -Default 'available' -Enum @('available', 'pending', 'sold') | ConvertTo-PodeOAParameter -In Query ) ) | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Pet' -Array) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet' -Array) -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid status value' - - - - - - - - Add-PodeRoute -PassThru -Method get -Path '/pet/findByTag' -Authentication 'test' -Scope 'read' -ScriptBlock { Write-PodeJsonResponse -Value 'done' -StatusCode 200 } | Set-PodeOARouteInfo -Summary 'Finds Pets by tags' -Description 'Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.' -Tags 'pet' -OperationId 'findPetsByTags' -PassThru | Set-PodeOARequest -PassThru -Parameters @( ( New-PodeOAStringProperty -Name 'tag' -Description 'Tags to filter by' -Array | ConvertTo-PodeOAParameter -In Query -Explode) ) | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Pet' ) -PassThru | #missing array application/json: + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet' ) -PassThru | #missing array application/json: # schema: # type: array # items: # $ref: '#/components/schemas/Pet' Add-PodeOAResponse -StatusCode 400 -Description 'Invalid status value' -PassThru | - Add-PodeOAResponse -Default -Description 'Unexpected error' -Content (New-PodeOAContentMediaType -MediaType 'application/json' -Content 'ErrorModel' ) + Add-PodeOAResponse -Default -Description 'Unexpected error' -Content (New-PodeOAContentMediaType -ContentType 'application/json' -Content 'ErrorModel' ) Add-PodeRoute -PassThru -Method Get -Path '/pet/:petId' -Authentication 'Login-OAuth2' -Scope 'read' -ScriptBlock { Write-PodeJsonResponse -Value 'done' -StatusCode 200 @@ -646,9 +709,7 @@ Some useful links: Set-PodeOARequest -PassThru -Parameters @(( ConvertTo-PodeOAParameter -Reference 'PetIdParam' ), ( New-PodeOAStringProperty -Name 'name' -Description 'Name of pet that needs to be updated' | ConvertTo-PodeOAParameter -In Query ) , ( New-PodeOAStringProperty -Name 'status' -Description 'Status of pet that needs to be updated' | ConvertTo-PodeOAParameter -In Query ) - ) -RequestBody ( - # New-PodeOARequestBody -Content @{ - # 'application/x-www-form-urlencoded' = (New-PodeOAObjectProperty -Properties @( (New-PodeOAStringProperty -format 'uuid'), (New-PodeOAObjectProperty -Properties @()))) + ) -RequestBody ( New-PodeOARequestBody -Properties -Content @{ 'multipart/form-data' = (New-PodeOAStringProperty -Name 'file' -Format binary -Array) }) | Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -PassThru | @@ -663,7 +724,7 @@ Some useful links: Set-PodeOARequest -PassThru -Parameters @(( ConvertTo-PodeOAParameter -Reference 'PetIdParam' ), ( New-PodeOAStringProperty -Name 'name' -Description 'Name of pet that needs to be updated' | ConvertTo-PodeOAParameter -In Query ) , ( New-PodeOAStringProperty -Name 'status' -Description 'Status of pet that needs to be updated' | ConvertTo-PodeOAParameter -In Query ) - ) -RequestBody ( + ) -RequestBody ( New-PodeOARequestBody -Content @{ 'application/x-www-form-urlencoded' = (New-PodeOAObjectProperty -Properties @( (New-PodeOAStringProperty -name 'id' -format 'uuid'), (New-PodeOAObjectProperty -name 'address' -NoProperties))) @@ -678,7 +739,7 @@ Some useful links: Set-PodeOARequest -PassThru -Parameters @(( ConvertTo-PodeOAParameter -Reference 'PetIdParam' ), ( New-PodeOAStringProperty -Name 'name' -Description 'Name of pet that needs to be updated' | ConvertTo-PodeOAParameter -In Query ) , ( New-PodeOAStringProperty -Name 'status' -Description 'Status of pet that needs to be updated' | ConvertTo-PodeOAParameter -In Query ) - ) -RequestBody (New-PodeOARequestBody -Content @{'multipart/form-data' = + ) -RequestBody (New-PodeOARequestBody -Content @{'multipart/form-data' = New-PodeOAStringProperty -name 'id' -format 'uuid' | New-PodeOAObjectProperty -name 'address' -NoProperties | New-PodeOAStringProperty -name 'children' -array | @@ -695,7 +756,7 @@ Some useful links: Set-PodeOARequest -PassThru -Parameters @(( ConvertTo-PodeOAParameter -Reference 'PetIdParam' ), ( New-PodeOAStringProperty -Name 'name' -Description 'Name of pet that needs to be updated' | ConvertTo-PodeOAParameter -In Query ) , ( New-PodeOAStringProperty -Name 'status' -Description 'Status of pet that needs to be updated' | ConvertTo-PodeOAParameter -In Query ) - ) -RequestBody (New-PodeOARequestBody -Content @{'multipart/form-data' = + ) -RequestBody (New-PodeOARequestBody -Content @{'multipart/form-data' = New-PodeOAStringProperty -name 'id' -format 'uuid' | New-PodeOAObjectProperty -name 'address' -NoProperties | New-PodeOAObjectProperty -name 'historyMetadata' -Description 'metadata in XML format' -NoProperties | @@ -708,7 +769,7 @@ Some useful links: New-PodeOAIntProperty -Name 'X-Rate-Limit-Reset' -Description 'The number of seconds left in the current period' -Minimum 2 ) ) - ) | Add-PodeOAResponse -StatusCode 200 -PassThru -Description 'A simple string response' -Content ( New-PodeOAContentMediaType -MediaType 'text/plain' -Content ( New-PodeOAStringProperty) ) | + ) | Add-PodeOAResponse -StatusCode 200 -PassThru -Description 'A simple string response' -Content ( New-PodeOAContentMediaType -ContentType 'text/plain' -Content ( New-PodeOAStringProperty)) | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' -PassThru | Add-PodeOAResponse -StatusCode 405 -Description 'Invalid Input' @@ -722,7 +783,7 @@ Some useful links: ( New-PodeOAStringProperty -Name 'additionalMetadata' -Description 'Additional Metadata' | ConvertTo-PodeOAParameter -In Query ) ) -RequestBody (New-PodeOARequestBody -Required -Content @{ 'multipart/form-data' = New-PodeOAObjectProperty -Properties @( (New-PodeOAStringProperty -Name 'image' -Format Binary )) } ) -PassThru | Add-PodeOAResponse -StatusCode 200 -Description 'A simple string response' -Content ( - New-PodeOAContentMediaType -MediaType 'text/plain' -Content ( New-PodeOAStringProperty -Example 'whoa!') ) -Headers ( + New-PodeOAContentMediaType -ContentType 'text/plain' -Content ( New-PodeOAStringProperty -Example 'whoa!')) -Headers ( New-PodeOAIntProperty -Name 'X-Rate-Limit-Limit' -Description 'The number of allowed requests in the current period' | New-PodeOAIntProperty -Name 'X-Rate-Limit-Remaining' -Description 'The number of remaining requests in the current period' | New-PodeOAIntProperty -Name 'X-Rate-Limit-Reset' -Description 'The number of seconds left in the current period' -Maximum 3 @@ -743,7 +804,7 @@ Some useful links: ( New-PodeOAIntProperty -Name 'petId' -Format Int64 -Description 'ID of pet that needs to be updated' -Required | ConvertTo-PodeOAParameter -In Path ), ( New-PodeOAStringProperty -Name 'additionalMetadata' -Description 'Additional Metadata' | ConvertTo-PodeOAParameter -In Query ) ) -RequestBody ( - New-PodeOARequestBody -Required -Content ( New-PodeOAContentMediaType -MediaType 'multipart/form-data' -Upload -PartContentMediaType 'application/octect-stream' -Content ( + New-PodeOARequestBody -Required -Content ( New-PodeOAContentMediaType -ContentType 'multipart/form-data' -Upload -PartContentMediaType 'application/octect-stream' -Content ( New-PodeOAIntProperty -name 'orderId' | New-PodeOAStringProperty -Name 'image' -Format Binary | New-PodeOAObjectProperty )) ) -PassThru | Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{'application/json' = 'ApiResponse' } -PassThru | @@ -758,7 +819,7 @@ Some useful links: ( New-PodeOAIntProperty -Name 'petId' -Format Int64 -Description 'ID of pet that needs to be updated' -Required | ConvertTo-PodeOAParameter -In Path ), ( New-PodeOAStringProperty -Name 'additionalMetadata' -Description 'Additional Metadata' | ConvertTo-PodeOAParameter -In Query ) ) -RequestBody ( - New-PodeOARequestBody -Required -Content ( New-PodeOAContentMediaType -MediaType 'application/octet-stream' -Upload ) + New-PodeOARequestBody -Required -Content ( New-PodeOAContentMediaType -ContentType 'application/octet-stream' -Upload ) ) -PassThru | Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{'application/json' = 'ApiResponse' } -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' -PassThru | @@ -773,7 +834,7 @@ Some useful links: Add-PodeRoute -PassThru -Method post -Path '/store/order' -ScriptBlock { Write-PodeJsonResponse -Value 'done' -StatusCode 200 } | Set-PodeOARouteInfo -Deprecated -Summary 'Place an order for a pet' -Description 'Place a new order in the store' -Tags 'store' -OperationId 'placeOrder' -PassThru | - Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Required -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'Order' )) -PassThru | + Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Required -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'Order' )) -PassThru | Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (@{ 'application/json' = 'Order' ; 'application/xml' = 'Order' }) -PassThru | Add-PodeOAResponse -StatusCode 405 -Description 'Invalid Input' @@ -789,7 +850,7 @@ Some useful links: Set-PodeOARequest -PassThru -Parameters @( ( New-PodeOAIntProperty -Name 'orderId' -Format Int64 -Description 'ID of order that needs to be fetched' -Required | ConvertTo-PodeOAParameter -In Path ) ) | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'Order' ) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'Order' ) -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' -PassThru | Add-PodeOAResponse -StatusCode 404 -Description 'Order not found' @@ -811,23 +872,24 @@ Some useful links: $User = $WebEvent.data $User.id = Get-Random -Minimum 1 -Maximum 9999999 Write-PodeJsonResponse -Value ($User | ConvertTo-Json -Depth 20 ) -StatusCode 200 - } else { + } + else { Write-PodeJsonResponse -StatusCode 405 -Value @{ result = $Validate.result message = $Validate.message -join ', ' } } } | Set-PodeOARouteInfo -Summary 'Create user.' -Description 'This can only be done by the logged in user.' -Tags 'user' -OperationId 'createUser' -PassThru | - Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Required -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'User' )) -PassThru | + Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Required -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'User' )) -PassThru | Add-PodeOAResponse -StatusCode 200 -Reference 'UserOpSuccess' -PassThru | Add-PodeOAResponse -StatusCode 405 -Description 'Invalid Input' -Content @{ - 'application/json' = (New-PodeOAObjectProperty -Properties @( (New-PodeOAStringProperty -Name 'result'), (New-PodeOAStringProperty -Name 'message') )) + 'application/json' = (New-PodeOAObjectProperty -Properties @((New-PodeOAStringProperty -Name 'result'), (New-PodeOAStringProperty -Name 'message'))) } Add-PodeRoute -PassThru -Method post -Path '/user/createWithList' -ScriptBlock { Write-PodeJsonResponse -Value 'done' -StatusCode 200 } | Set-PodeOARouteInfo -Summary 'Creates list of users with given input array.' -Description 'Creates list of users with given input array.' -Tags 'user' -OperationId 'createUsersWithListInput' -PassThru | - Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Required -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'User' )) -PassThru | + Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Required -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'User' )) -PassThru | Add-PodeOAResponse -StatusCode 200 -Reference 'UserOpSuccess' -PassThru | Add-PodeOAResponse -StatusCode 405 -Description 'Invalid Input' @@ -838,7 +900,7 @@ Some useful links: ( New-PodeOAStringProperty -Name 'username' -Description 'The user name for login' | ConvertTo-PodeOAParameter -In Query ) ( New-PodeOAStringProperty -Name 'password' -Description 'The password for login in clear text' -Format Password | ConvertTo-PodeOAParameter -In Query ) ) | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'string' ) ` + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'string' ) ` -Header @('X-Rate-Limit', 'X-Expires-After') -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid username/password supplied' @@ -869,7 +931,7 @@ Some useful links: } | Set-PodeOARouteInfo -Summary 'Update user' -Description 'This can only be done by the logged in user.' -Tags 'user' -OperationId 'updateUser_1' -PassThru | Set-PodeOARequest -Parameters @( ( New-PodeOAStringProperty -Name 'username' -Description ' name that need to be updated.' -Required | ConvertTo-PodeOAParameter -In Path ) - ) -RequestBody (New-PodeOARequestBody -Required -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'StructPart' )) + ) -RequestBody (New-PodeOARequestBody -Required -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'StructPart' )) Add-PodeRoute -PassThru -Method Put -Path '/user/:username' -ScriptBlock { @@ -877,7 +939,7 @@ Some useful links: } | Set-PodeOARouteInfo -Summary 'Update user' -Description 'This can only be done by the logged in user.' -Tags 'user' -OperationId 'updateUser' -PassThru | Set-PodeOARequest -Parameters @( ( New-PodeOAStringProperty -Name 'username' -Description ' name that need to be updated.' -Required | ConvertTo-PodeOAParameter -In Path ) - ) -RequestBody (New-PodeOARequestBody -Required -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'User' )) -PassThru | + ) -RequestBody (New-PodeOARequestBody -Required -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'User' )) -PassThru | Add-PodeOAResponse -StatusCode 200 -Reference 'UserOpSuccess' -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid username supplied' -PassThru | Add-PodeOAResponse -StatusCode 404 -Description 'User not found' -PassThru | @@ -890,7 +952,7 @@ Some useful links: } | Set-PodeOARouteInfo -Summary 'Update user' -Description 'This can only be done by the logged in user.' -Tags 'user' -OperationId 'updateUserLink' -PassThru | Set-PodeOARequest -Parameters @( ( New-PodeOAStringProperty -Name 'username' -Description ' name that need to be updated.' -Required | ConvertTo-PodeOAParameter -In Path ) - ) -RequestBody (New-PodeOARequestBody -Required -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'User' )) -PassThru | + ) -RequestBody (New-PodeOARequestBody -Required -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'User' )) -PassThru | Add-PodeOAResponse -StatusCode 200 -Content @{'application/json' = 'User' } -PassThru ` -Links (New-PodeOAResponseLink -Name address -OperationId 'getUserByName' -Parameters @{'username' = '$request.path.username' } ) | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid username supplied' -PassThru | @@ -906,14 +968,14 @@ Some useful links: } | Set-PodeOARouteInfo -Summary 'Update user' -Description 'This can only be done by the logged in user.' -Tags 'user' -OperationId 'updateUserLinkByRef' -PassThru | Set-PodeOARequest -Parameters @( ( New-PodeOAStringProperty -Name 'username' -Description ' name that need to be updated.' -Required | ConvertTo-PodeOAParameter -In Path ) - ) -RequestBody (New-PodeOARequestBody -Required -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'User' )) -PassThru | + ) -RequestBody (New-PodeOARequestBody -Required -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'User' )) -PassThru | Add-PodeOAResponse -StatusCode 200 -Content @{'application/json' = 'User' } -PassThru ` -Links (New-PodeOAResponseLink -Name 'address2' -Reference 'address' ) | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid username supplied' -PassThru | Add-PodeOAResponse -StatusCode 404 -Description 'User not found' -PassThru | Add-PodeOAResponse -StatusCode 405 -Description 'Invalid Input' - + #region Test remove route Add-PodeRoute -PassThru -Method Delete -Path '/usera/:username' -ScriptBlock { Write-PodeJsonResponse -Value 'done' -StatusCode 200 @@ -928,7 +990,6 @@ Some useful links: Remove-PodeRoute -Method Delete -Path '/api/v3/usera/:username' - Add-PodeRoute -PassThru -Method Delete -Path '/user/:username' -ScriptBlock { Write-PodeJsonResponse -Value 'done' -StatusCode 200 } | Set-PodeOARouteInfo -Summary 'Delete user' -Description 'This can only be done by the logged in user.' -Tags 'user' -OperationId 'deleteUser' -PassThru | @@ -938,7 +999,7 @@ Some useful links: Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid username supplied' -PassThru | Add-PodeOAResponse -StatusCode 404 -Description 'User not found' - + #endregion Add-PodeOAExternalRoute -Method Get -Path '/stores/order/:orderId' -Servers ( @@ -949,7 +1010,7 @@ Some useful links: Set-PodeOARequest -PassThru -Parameters @( ( New-PodeOAIntProperty -Name 'orderId' -Format Int64 -Description 'ID of order that needs to be fetched' -Required | ConvertTo-PodeOAParameter -In Path ) ) | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'Order' ) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'Order' ) -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' -PassThru | Add-PodeOAResponse -StatusCode 404 -Description 'Order not found' } @@ -965,8 +1026,8 @@ Some useful links: } - $yaml = PodeOADefinition -Format Yaml -DefinitionTag 'v3.1' - $json = PodeOADefinition -Format Json -DefinitionTag 'v3' + $yaml = Get-PodeOADefinition -Format Yaml -DefinitionTag 'v3.1' + $json = Get-PodeOADefinition -Format Json -DefinitionTag 'v3' Write-PodeHost "`rYAML Tag: v3.1 Output:`r $yaml" diff --git a/examples/PetStore/Petstore-openApi.ps1 b/examples/PetStore/Petstore-OpenApi.ps1 similarity index 85% rename from examples/PetStore/Petstore-openApi.ps1 rename to examples/PetStore/Petstore-OpenApi.ps1 index dafd4dfd3..bf2578bf5 100644 --- a/examples/PetStore/Petstore-openApi.ps1 +++ b/examples/PetStore/Petstore-OpenApi.ps1 @@ -1,33 +1,67 @@ +<# +.SYNOPSIS + PowerShell script to set up a Pode server for a Pet Store API using OpenAPI 3.0 specifications. + +.DESCRIPTION + This script sets up a Pode server that listens on a specified port and uses OpenAPI 3.0 specifications + for defining the API. It supports multiple endpoints for managing pets, orders, and users with various + authentication methods including API key, Basic, and OAuth2. + + This example shows how to use session persistent authentication using Windows Active Directory. + The example used here is Form authentication, sent from the
in HTML. + + Navigating to the 'http://localhost:8081' endpoint in your browser will auto-redirect you to the '/login' + page. Here, you can type the details for a domain user. Clicking 'Login' will take you back to the home + page with a greeting and a view counter. Clicking 'Logout' will purge the session and take you back to + the login page. + +.PARAMETER Reset + Switch parameter to reset the PetData.json file and reinitialize categories, pets, orders, and users. + +.EXAMPLE + To run the sample: ./PetStore/Petstore-OpenApi.ps1 + + OpenAPI Info: + Specification: + http://localhost:8081/openapi + Documentation: + http://localhost:8081/docs + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/PetStore/Petstore-OpenApi.ps1 +.NOTES + Author: Pode Team + License: MIT License +#> + param ( [switch] $Reset ) -$petStorePath = Split-Path -Parent -Path $MyInvocation.MyCommand.Path -$podePath = Split-Path -Parent -Path (Split-Path -Parent -Path $petStorePath) -if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { - Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop -} -else { - Import-Module -Name 'Pode' -} -function Write-ObjectContent { - param ( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - $Object - ) - Write-PodeHost -ForegroundColor Blue "Type:$($Object.gettype())" - $objectString = $Object | Out-String - Write-PodeHost -ForegroundColor Blue -Object $objectString +try { + # Determine paths for the Pode module and Pet Store + $petStorePath = Split-Path -Parent -Path $MyInvocation.MyCommand.Path + $podePath = Split-Path -Parent -Path (Split-Path -Parent -Path $petStorePath) -} + # Import Pode module from source path or from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } -Import-Module -Name "$petStorePath/PetData.psm1" -Import-Module -Name "$petStorePath/Order.psm1" -Import-Module -Name "$petStorePath/UserData.psm1" + # Import additional modules for PetData, Order, and UserData + Import-Module -Name "$petStorePath/PetData.psm1" -ErrorAction Stop + Import-Module -Name "$petStorePath/Order.psm1" -ErrorAction Stop + Import-Module -Name "$petStorePath/UserData.psm1" -ErrorAction Stop +} +catch { throw } +# Start Pode server with specified script block Start-PodeServer -Threads 1 -ScriptBlock { - + # Define paths for data, images, and certificates $script:PetDataPath = Join-Path -Path $PetStorePath -ChildPath 'data' If (!(Test-Path -PathType container -Path $script:PetDataPath)) { New-Item -ItemType Directory -Path $script:PetDataPath -Force | Out-Null @@ -43,9 +77,9 @@ Start-PodeServer -Threads 1 -ScriptBlock { New-Item -ItemType Directory -Path $script:CertsPath -Force | Out-Null } + # Load data from JSON file or initialize data if Reset switch is present - #Load data - $script:PetDataJson = Join-Path -Path $PetDataPath -ChildPath 'PetData.json' + $script:PetDataJson = Join-Path -Path $PetDataPath -ChildPath 'PetData.json' if ($Reset.IsPresent -or !(Test-Path -Path $script:PetDataJson -PathType Leaf )) { Initialize-Categories -Reset Initialize-Pet -Reset @@ -58,11 +92,11 @@ Start-PodeServer -Threads 1 -ScriptBlock { Initialize-Pet Initialize-Order Initialize-Users - # attempt to re-initialise the state (will do nothing if the file doesn't exist) + # attempt to re-initialise the state (will do nothing if the file doesn't exist) Restore-PodeState -Path $script:PetDataJson } - + # Configure Pode server endpoints if ((Get-PodeConfig).Protocol -eq 'Https') { $Certificate = Join-Path -Path $CertsPath -ChildPath (Get-PodeConfig).Certificate $CertificateKey = Join-Path -Path $CertsPath -ChildPath (Get-PodeConfig).CertificateKey @@ -72,24 +106,27 @@ Start-PodeServer -Threads 1 -ScriptBlock { Add-PodeEndpoint -Address (Get-PodeConfig).Address -Port (Get-PodeConfig).RestFulPort -Protocol Http -Default } + # Enable error logging New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - #Configure CORS - Set-PodeSecurityAccessControl -Origin '*' -Duration 7200 -WithOptions -AuthorizationHeader -autoMethods -AutoHeader -Credentials -CrossDomainXhrRequests #-Header 'content-type' # -Header 'Accept','Content-Type' ,'Connection' #-Headers '*' 'x-requested-with' ,'crossdomain'# + # Configure CORS + Set-PodeSecurityAccessControl -Origin '*' -Duration 7200 -WithOptions -AuthorizationHeader -autoMethods -AutoHeader -Credentials -CrossDomainXhrRequests + # Add static route for images - #image folder Add-PodeStaticRoute -Path '/images' -Source $script:PetImagesPath + # Enable OpenAPI documentation + Enable-PodeOpenApi -Path '/docs/openapi' -OpenApiVersion '3.0.3' -EnableSchemaValidation:($PSVersionTable.PSEdition -eq 'Core') -DisableMinimalDefinitions -NoDefaultResponses - Enable-PodeOpenApi -Path '/docs/openapi' -OpenApiVersion '3.0.3' -EnableSchemaValidation -DisableMinimalDefinitions -NoDefaultResponses - + # Add external documentation link for Swagger $swaggerDocs = New-PodeOAExternalDoc -Description 'Find out more about Swagger' -Url 'http://swagger.io' $swaggerDocs | Add-PodeOAExternalDoc + # Add OpenAPI information $InfoDescription = @' -This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about Swagger at [http://swagger.io](http://swagger.io). +This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about Swagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach! You can now help us improve the API whether it's by making changes to the definition itself or to the code. That way, with time, we can improve the API in general, and expose some of the new features in OAS3. @@ -100,11 +137,11 @@ Some useful links: '@ - Add-PodeOAInfo -Title 'Swagger Petstore - OpenAPI 3.0' -Version 1.0.17 -Description $InfoDescription -TermsOfService 'http://swagger.io/terms/' -LicenseName 'Apache 2.0' ` + Add-PodeOAInfo -Title 'Swagger Petstore - OpenAPI 3.0.3' -Version 1.0.17 -Description $InfoDescription -TermsOfService 'http://swagger.io/terms/' -LicenseName 'Apache 2.0' ` -LicenseUrl 'http://www.apache.org/licenses/LICENSE-2.0.html' -ContactName 'API Support' -ContactEmail 'apiteam@swagger.io' Add-PodeOAServerEndpoint -url '/api/v3' -Description 'default endpoint' - + # Enable OpenAPI viewers Enable-PodeOAViewer -Type Swagger -Path '/docs/swagger' Enable-PodeOAViewer -Type ReDoc -Path '/docs/redoc' -DarkMode Enable-PodeOAViewer -Type RapiDoc -Path '/docs/rapidoc' -DarkMode @@ -112,26 +149,30 @@ Some useful links: Enable-PodeOAViewer -Type Explorer -Path '/docs/explorer' -DarkMode Enable-PodeOAViewer -Type RapiPdf -Path '/docs/rapipdf' -DarkMode + # Enable OpenAPI editor and bookmarks Enable-PodeOAViewer -Editor -Path '/docs/swagger-editor' Enable-PodeOAViewer -Bookmarks -Path '/docs' - # setup session details + # Setup session details Enable-PodeSessionMiddleware -Duration 120 -Extend + # Define access schemes and authentication New-PodeAccessScheme -Type Scope | Add-PodeAccess -Name 'read:pets' -Description 'read your pets' New-PodeAccessScheme -Type Scope | Add-PodeAccess -Name 'write:pets' -Description 'modify pets in your account' $clientId = '123123123' $clientSecret = '' - New-PodeAuthScheme -OAuth2 -ClientId $ClientId -ClientSecret $ClientSecret ` + # OAuth2 authentication + New-PodeAuthScheme -OAuth2 -ClientId $ClientId -ClientSecret $ClientSecret ` -AuthoriseUrl 'https://petstore3.swagger.io/oauth/authorize' ` -TokenUrl 'https://petstore3.swagger.io/oauth/token' ` -Scope 'read:pets', 'write:pets' | - Add-PodeAuth -Name 'petstore_auth' -FailureUrl 'https://petstore3.swagger.io/oauth/failure' -SuccessUrl '/' -ScriptBlock { + Add-PodeAuth -Name 'petstore_auth' -FailureUrl 'https://petstore3.swagger.io/oauth/failure' -SuccessUrl '/' -ScriptBlock { param($user, $accessToken, $refreshToken) return @{ User = $user } } + # API key authentication New-PodeAuthScheme -ApiKey -LocationName 'api_key' | Add-PodeAuth -Name 'api_key' -Sessionless -ScriptBlock { param($key) if ($key) { @@ -161,6 +202,7 @@ Some useful links: } } + # Basic authentication New-PodeAuthScheme -Basic -Realm 'PetStore' | Add-PodeAuth -Name 'Basic' -Sessionless -ScriptBlock { param($username, $password) @@ -179,15 +221,17 @@ Some useful links: return @{ Message = 'Invalid details supplied' } } - Merge-PodeAuth -Name 'merged_auth' -Authentication 'Basic', 'api_key' -Valid One - Merge-PodeAuth -Name 'merged_auth_All' -Authentication 'Basic', 'api_key' -Valid All -ScriptBlock {} - Merge-PodeAuth -Name 'merged_auth_nokey' -Authentication 'Basic' -Valid One + # Merge authentication schemes + Merge-PodeAuth -Name 'merged_auth' -Authentication 'Basic', 'api_key' -Valid One + Merge-PodeAuth -Name 'merged_auth_All' -Authentication 'Basic', 'api_key' -Valid All -ScriptBlock {} + Merge-PodeAuth -Name 'merged_auth_nokey' -Authentication 'Basic' -Valid One + # Add OpenAPI tags Add-PodeOATag -Name 'user' -Description 'Operations about user' Add-PodeOATag -Name 'store' -Description 'Access to Petstore orders' -ExternalDoc $swaggerDocs Add-PodeOATag -Name 'pet' -Description 'Everything about your Pets' -ExternalDoc $swaggerDocs - + # Define OpenAPI component schemas New-PodeOAIntProperty -Name 'id'-Format Int64 -Example 10 -Required | New-PodeOAIntProperty -Name 'petId' -Format Int64 -Example 198772 -Required | New-PodeOAIntProperty -Name 'quantity' -Format Int32 -Example 7 -Required | @@ -201,11 +245,11 @@ Some useful links: New-PodeOAStringProperty -Name 'city' -Example 'Palo Alto' -Required | New-PodeOAStringProperty -Name 'state' -Example 'CA' -Required | New-PodeOAStringProperty -Name 'zip' -Example '94031' -Required | - New-PodeOAObjectProperty -XmlName 'address' | + New-PodeOAObjectProperty -XmlName 'address' | Add-PodeOAComponentSchema -Name 'Address' New-PodeOAIntProperty -Name 'id'-Format Int64 -Example 100000 | - New-PodeOAStringProperty -Name 'username' -example 'fehguy' | + New-PodeOAStringProperty -Name 'username' -example 'fehguy' | New-PodeOASchemaProperty -Name 'Address' -Reference 'Address' -Array -XmlName 'addresses' -XmlWrapped | New-PodeOAObjectProperty -XmlName 'customer' | Add-PodeOAComponentSchema -Name 'Customer' @@ -213,7 +257,7 @@ Some useful links: New-PodeOAIntProperty -Name 'id'-Format Int64 -Example 1 | New-PodeOAStringProperty -Name 'name' -Example 'Dogs' | - New-PodeOAObjectProperty -XmlName 'category' | + New-PodeOAObjectProperty -XmlName 'category' | Add-PodeOAComponentSchema -Name 'Category' New-PodeOAIntProperty -Name 'id'-Format Int64 -Example 10 | @@ -228,8 +272,6 @@ Some useful links: New-PodeOAObjectProperty -XmlName 'user' | Add-PodeOAComponentSchema -Name 'User' - - New-PodeOAIntProperty -Name 'id'-Format Int64 | New-PodeOAStringProperty -Name 'name' | New-PodeOAObjectProperty -XmlName 'tag' | @@ -244,24 +286,22 @@ Some useful links: New-PodeOAObjectProperty -XmlName 'pet' | Add-PodeOAComponentSchema -Name 'Pet' - - New-PodeOAIntProperty -Name 'code'-Format Int32 | New-PodeOAStringProperty -Name 'type' | New-PodeOAStringProperty -Name 'message' | - New-PodeOAObjectProperty -XmlName '##default' | + New-PodeOAObjectProperty -XmlName '##default' | Add-PodeOAComponentSchema -Name 'ApiResponse' - + # Add OpenAPI component request bodies Add-PodeOAComponentRequestBody -Name 'Pet' -Description 'Pet object that needs to be added to the store' -Content ( - New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Pet') + New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet') Add-PodeOAComponentRequestBody -Name 'UserArray' -Description 'List of user object' -Content ( - New-PodeOAContentMediaType -MediaType 'application/json' -Content 'User' -Array) - + New-PodeOAContentMediaType -ContentType 'application/json' -Content 'User' -Array) + # Define API routes Add-PodeRouteGroup -Path '/api/v3' -Routes { <# PUT '/pet' @@ -303,9 +343,9 @@ Some useful links: } | Set-PodeOARouteInfo -Summary 'Update an existing pet' -Description 'Update an existing pet by Id' -Tags 'pet' -OperationId 'updatePet' -PassThru | Set-PodeOARequest -RequestBody ( New-PodeOARequestBody -Description 'Update an existent pet in the store' -Required -Content ( - New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Pet' ) + New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet' ) ) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Pet' ) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet' ) -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' -PassThru | Add-PodeOAResponse -StatusCode 404 -Description 'Pet not found' -PassThru | Add-PodeOAResponse -StatusCode 405 -Description 'Validation exception' @@ -341,9 +381,9 @@ Some useful links: } } | Set-PodeOARouteInfo -Summary 'Add a new pet to the store' -Description 'Add a new pet to the store' -Tags 'pet' -OperationId 'addPet' -PassThru | Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Description 'Create a new pet in the store' -Required -Content ( - New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Pet' ) + New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet' ) ) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Pet' ) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet' ) -PassThru | Add-PodeOAResponse -StatusCode 405 -Description 'Invalid input' @@ -372,7 +412,7 @@ Some useful links: Set-PodeOARequest -PassThru -Parameters ( New-PodeOAStringProperty -Name 'status' -Description 'Status values that need to be considered for filter' -Default 'available' -Enum @('available', 'pending', 'sold') | ConvertTo-PodeOAParameter -In Query -Explode ) | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Pet' -Array) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet' -Array) -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid status value' -PassThru | Add-PodeOAResponse -StatusCode 415 @@ -400,7 +440,7 @@ Some useful links: Set-PodeOARequest -PassThru -Parameters ( New-PodeOAStringProperty -Name 'tags' -Description 'Tags to filter by' -Array | ConvertTo-PodeOAParameter -In Query -Explode ) | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Pet' -Array) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet' -Array) -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid tag value' -PassThru | Add-PodeOAResponse -StatusCode 415 @@ -433,7 +473,7 @@ Some useful links: Set-PodeOARequest -PassThru -Parameters ( New-PodeOAIntProperty -Name 'petId' -Description 'ID of pet to return' -Format Int64 | ConvertTo-PodeOAParameter -In Path -Required ) | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Pet') -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet') -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' -PassThru | Add-PodeOAResponse -StatusCode 404 -Description 'Pet not found' -PassThru | Add-PodeOAResponse -StatusCode 415 @@ -510,7 +550,7 @@ Some useful links: ( New-PodeOAIntProperty -Name 'petId' -Format Int64 -Description 'ID of pet to update' -Required | ConvertTo-PodeOAParameter -In Path ), ( New-PodeOAStringProperty -Name 'additionalMetadata' -Description 'Additional Metadata' | ConvertTo-PodeOAParameter -In Query ) ) -RequestBody ( - New-PodeOARequestBody -Content ( New-PodeOAContentMediaType -MediaType 'application/octet-stream' -Upload ) + New-PodeOARequestBody -Content ( New-PodeOAContentMediaType -ContentType 'application/octet-stream' -Upload ) ) -PassThru | Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{'application/json' = 'ApiResponse' } @@ -558,7 +598,7 @@ Some useful links: Write-PodeHtmlResponse -StatusCode 405 -Value ($Validate.message -join ', ') } } | Set-PodeOARouteInfo -Summary 'Place an order for a pet' -Description 'Place a new order in the store' -Tags 'store' -OperationId 'placeOrder' -PassThru | - Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'Order' )) -PassThru | + Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'Order' )) -PassThru | Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (@{ 'application/json' = 'Order' }) -PassThru | Add-PodeOAResponse -StatusCode 405 -Description 'Invalid Input' @@ -588,7 +628,7 @@ Some useful links: Set-PodeOARequest -PassThru -Parameters @( ( New-PodeOAIntProperty -Name 'orderId' -Format Int64 -Description 'ID of order that needs to be fetched' -Required | ConvertTo-PodeOAParameter -In Path ) ) | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Order' ) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Order' ) -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' -PassThru | Add-PodeOAResponse -StatusCode 404 -Description 'Order not found' @@ -656,9 +696,9 @@ Some useful links: Write-PodeHtmlResponse -StatusCode 405 -Value ($Validate.message -join ', ') } } | Set-PodeOARouteInfo -Summary 'Create user.' -Description 'This can only be done by the logged in user.' -Tags 'user' -OperationId 'createUser' -PassThru | - Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'User' )) -PassThru | + Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'User' )) -PassThru | Add-PodeOAResponse -StatusCode 405 -Description 'Invalid Input' -PassThru | - Add-PodeOAResponse -Default -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'User' ) + Add-PodeOAResponse -Default -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'User' ) <# @@ -702,8 +742,8 @@ Some useful links: default { Write-PodeHtmlResponse -StatusCode 415 } } } | Set-PodeOARouteInfo -Summary 'Creates list of users with given input array.' -Description 'Creates list of users with given input array.' -Tags 'user' -OperationId 'createUsersWithListInput' -PassThru | - Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Content (New-PodeOAContentMediaType -MediaType 'application/json' -Content 'User' -Array)) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'User' -Array ) -PassThru | + Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Content (New-PodeOAContentMediaType -ContentType 'application/json' -Content 'User' -Array)) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'User' -Array ) -PassThru | Add-PodeOAResponse -Default -Description 'successful operation' @@ -736,7 +776,7 @@ Some useful links: } | Set-PodeOARouteInfo -Summary 'Logs user into the system.' -Tags 'user' -OperationId 'loginUser' -PassThru | Set-PodeOARequest -Parameters ( New-PodeOAStringProperty -Name 'username' -Description 'The user name for login' | ConvertTo-PodeOAParameter -In Query ), ( New-PodeOAStringProperty -Name 'password' -Description 'The password for login in clear text' -Format Password | ConvertTo-PodeOAParameter -In Query ) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'string' ) ` + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'string' ) ` -Headers (New-PodeOAIntProperty -Name 'X-Rate-Limit' -Description 'calls per hour allowed by the user' -Format Int32), (New-PodeOAStringProperty -Name 'X-Expires-After' -Description 'date in UTC when token expires' -Format Date-Time) -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid username/password supplied' @@ -774,7 +814,7 @@ Some useful links: } } | Set-PodeOARouteInfo -Summary 'Get user by user name' -Tags 'user' -OperationId 'getUserByName' -PassThru | Set-PodeOARequest -Parameters ( New-PodeOAStringProperty -Name 'username' -Description 'The name that needs to be fetched. Use user1 for testing.' -Required | ConvertTo-PodeOAParameter -In Path ) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'User' ) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'User' ) -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid username supplied' -PassThru | Add-PodeOAResponse -StatusCode 404 -Description 'User not found' @@ -823,7 +863,7 @@ Some useful links: } | Set-PodeOARouteInfo -Summary 'Update user' -Description 'This can only be done by the logged in user.' -Tags 'user' -OperationId 'updateUser' -PassThru | Set-PodeOARequest -Parameters ( New-PodeOAStringProperty -Name 'username' -Description ' name that need to be updated.' -Required | ConvertTo-PodeOAParameter -In Path ) ` -RequestBody ( New-PodeOARequestBody -Required -Description 'Update an existent user in the store' -Content ( - New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'User' + New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'User' )) -PassThru | Add-PodeOAResponse -StatusCode 405 -Description 'Invalid Input' -PassThru | Add-PodeOAResponse -StatusCode 404 -Description 'User not found' -PassThru | @@ -847,7 +887,7 @@ Some useful links: Write-PodeJsonReWrite-PodeHtmlResponsesponse -Value 'Invalid username supplied' -StatusCode 400 } } | Set-PodeOARouteInfo -Summary 'Delete user' -Description 'This can only be done by the logged in user.' -Tags 'user' -OperationId 'deleteUser' -PassThru | - Set-PodeOARequest -Parameters ( New-PodeOAStringProperty -Name 'username' -Description 'The name that needs to be deleted.' -Required | ConvertTo-PodeOAParameter -In Path ) -PassThru | + Set-PodeOARequest -Parameters ( New-PodeOAStringProperty -Name 'username' -Description 'The name that needs to be deleted.' -Required | ConvertTo-PodeOAParameter -In Path ) -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid username supplied' -PassThru | Add-PodeOAResponse -StatusCode 404 -Description 'User not found' } diff --git a/examples/PetStore/Petstore-openApiMultiTag.ps1 b/examples/PetStore/Petstore-OpenApiMultiTag.ps1 similarity index 88% rename from examples/PetStore/Petstore-openApiMultiTag.ps1 rename to examples/PetStore/Petstore-OpenApiMultiTag.ps1 index c3461032c..932b3b0aa 100644 --- a/examples/PetStore/Petstore-openApiMultiTag.ps1 +++ b/examples/PetStore/Petstore-OpenApiMultiTag.ps1 @@ -1,19 +1,54 @@ + +<# +.SYNOPSIS + Sets up a Pode server for a Pet Store API with OpenAPI 3.0 and 3.1 specifications. + +.DESCRIPTION + This script configures a Pode server to serve a Pet Store API using OpenAPI 3.0 and 3.1 specifications. + It includes multiple endpoints for managing pets, orders, and users, with support for different authentication + methods such as API key, Basic, and OAuth2. The server also supports session persistent authentication using + Windows Active Directory and provides OpenAPI documentation viewers. + +.PARAMETER Reset + Switch parameter to reset the PetData.json file and reinitialize categories, pets, orders, and users. + +.EXAMPLE + To run the sample: ./PetStore/Petstore-OpenApiMultiTag.ps1 + + Using a browser to access the OpenAPI Info: + 'v3.1': http://127.0.0.1:8082/docs/openapi/v3.1 + Documentation: http://127.0.0.1:8082/docs/v3.1 + + 'v3.0.3': http://127.0.0.1:8081/docs/openapi/v3.0 + Documentation: http://127.0.0.1:8081/docs + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/PetStore/Petstore-OpenApiMultiTag.ps1 +.NOTES + Author: Pode Team + License: MIT License +#> param ( [switch] $Reset ) -$petStorePath = Split-Path -Parent -Path $MyInvocation.MyCommand.Path -$podePath = Split-Path -Parent -Path (Split-Path -Parent -Path $petStorePath) -if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { - Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop -} -else { - Import-Module -Name 'Pode' -} -Import-Module -Name "$petStorePath/PetData.psm1" -Import-Module -Name "$petStorePath/Order.psm1" -Import-Module -Name "$petStorePath/UserData.psm1" +try { + $petStorePath = Split-Path -Parent -Path $MyInvocation.MyCommand.Path + $podePath = Split-Path -Parent -Path (Split-Path -Parent -Path $petStorePath) + + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } + + Import-Module -Name "$petStorePath/PetData.psm1" -ErrorAction Stop + Import-Module -Name "$petStorePath/Order.psm1" -ErrorAction Stop + Import-Module -Name "$petStorePath/UserData.psm1" -ErrorAction Stop +} +catch { throw } Start-PodeServer -Threads 1 -ScriptBlock { @@ -72,8 +107,8 @@ Start-PodeServer -Threads 1 -ScriptBlock { - Enable-PodeOpenApi -Path '/docs/openapi/v3.0' -OpenApiVersion '3.0.2' -EnableSchemaValidation -DisableMinimalDefinitions -NoDefaultResponses -EndpointName 'endpoint_v3' - Enable-PodeOpenApi -Path '/docs/openapi/v3.1' -OpenApiVersion '3.1.0' -EnableSchemaValidation -DisableMinimalDefinitions -NoDefaultResponses -DefinitionTag 'v3.1' -EndpointName 'endpoint_v3.1' + Enable-PodeOpenApi -Path '/docs/openapi/v3.0' -OpenApiVersion '3.0.3' -EnableSchemaValidation:($PSVersionTable.PSEdition -eq 'Core') -DisableMinimalDefinitions -NoDefaultResponses -EndpointName 'endpoint_v3' + Enable-PodeOpenApi -Path '/docs/openapi/v3.1' -OpenApiVersion '3.1.0' -EnableSchemaValidation:($PSVersionTable.PSEdition -eq 'Core') -DisableMinimalDefinitions -NoDefaultResponses -DefinitionTag 'v3.1' -EndpointName 'endpoint_v3.1' $swaggerDocs = New-PodeOAExternalDoc -Description 'Find out more about Swagger' -Url 'http://swagger.io' $swaggerDocs | Add-PodeOAExternalDoc -DefinitionTag 'v3.0.3', 'v3.1' @@ -91,31 +126,33 @@ Some useful links: '@ - Add-PodeOAInfo -Title 'Swagger Petstore - OpenAPI 3.0' -Version 1.0.17 -Description $InfoDescription -TermsOfService 'http://swagger.io/terms/' -LicenseName 'Apache 2.0' ` + Add-PodeOAInfo -Title 'Swagger Petstore - OpenAPI 3.0.3' -Version 1.0.17 -Description $InfoDescription -TermsOfService 'http://swagger.io/terms/' -LicenseName 'Apache 2.0' ` -LicenseUrl 'http://www.apache.org/licenses/LICENSE-2.0.html' -ContactName 'API Support' -ContactEmail 'apiteam@swagger.io' -DefinitionTag 'v3.0.3' - Add-PodeOAInfo -Title 'Swagger Petstore - OpenAPI 3.1' -Version 1.0.17 -Description $InfoDescription -TermsOfService 'http://swagger.io/terms/' -LicenseName 'Apache 2.0' ` + Add-PodeOAInfo -Title 'Swagger Petstore - OpenAPI 3.1.0' -Version 1.0.17 -Description $InfoDescription -TermsOfService 'http://swagger.io/terms/' -LicenseName 'Apache 2.0' ` -LicenseUrl 'http://www.apache.org/licenses/LICENSE-2.0.html' -ContactName 'API Support' -ContactEmail 'apiteam@swagger.io' -DefinitionTag 'v3.1' - Add-PodeOAServerEndpoint -url '/api/v3' -Description 'default endpoint' -DefinitionTag 'v3.0.3', 'v3.1' + Add-PodeOAServerEndpoint -url '/api/v3' -Description 'V3 Endpoint' -DefinitionTag 'v3.0.3' + Add-PodeOAServerEndpoint -url '/api/v3' -Description 'V3.1 Endpoint' -DefinitionTag 'v3.1' + Add-PodeOAServerEndpoint -url '/api' -Description 'Default Endpoint' -DefinitionTag 'v3.0.3','v3.1' #OpenAPI 3.0 - Enable-PodeOAViewer -Type Swagger -Path '/docs/swagger' -DefinitionTag 'v3.0.3' -EndpointName 'endpoint_v3' - Enable-PodeOAViewer -Type ReDoc -Path '/docs/redoc' -DarkMode -DefinitionTag 'v3.0.3' -EndpointName 'endpoint_v3' - Enable-PodeOAViewer -Type RapiDoc -Path '/docs/rapidoc' -DarkMode -DefinitionTag 'v3.0.3' -EndpointName 'endpoint_v3' - Enable-PodeOAViewer -Type StopLight -Path '/docs/stoplight' -DarkMode -DefinitionTag 'v3.0.3' -EndpointName 'endpoint_v3' - Enable-PodeOAViewer -Type Explorer -Path '/docs/explorer' -DarkMode -DefinitionTag 'v3.0.3' -EndpointName 'endpoint_v3' - Enable-PodeOAViewer -Type RapiPdf -Path '/docs/rapipdf' -DarkMode -DefinitionTag 'v3.0.3' -EndpointName 'endpoint_v3' - Enable-PodeOAViewer -Bookmarks -Path '/docs' -DefinitionTag 'v3.0.3' -EndpointName 'endpoint_v3' - Enable-PodeOAViewer -Editor -Path '/docs/swagger-editor' -DefinitionTag 'v3.0.3' -EndpointName 'endpoint_v3' + Enable-PodeOAViewer -Type Swagger -Path '/docs/swagger' -DefinitionTag 'v3.0.3' + Enable-PodeOAViewer -Type ReDoc -Path '/docs/redoc' -DarkMode -DefinitionTag 'v3.0.3' + Enable-PodeOAViewer -Type RapiDoc -Path '/docs/rapidoc' -DarkMode -DefinitionTag 'v3.0.3' + Enable-PodeOAViewer -Type StopLight -Path '/docs/stoplight' -DarkMode -DefinitionTag 'v3.0.3' + Enable-PodeOAViewer -Type Explorer -Path '/docs/explorer' -DarkMode -DefinitionTag 'v3.0.3' + Enable-PodeOAViewer -Type RapiPdf -Path '/docs/rapipdf' -DarkMode -DefinitionTag 'v3.0.3' + Enable-PodeOAViewer -Bookmarks -Path '/docs' -DefinitionTag 'v3.0.3' + Enable-PodeOAViewer -Editor -Path '/docs/swagger-editor' -DefinitionTag 'v3.0.3' #OpenAPI 3.1 - Enable-PodeOAViewer -Type Swagger -Path '/docs/swagger' -DefinitionTag 'v3.1' -EndpointName 'endpoint_v3.1' - Enable-PodeOAViewer -Type ReDoc -Path '/docs/vredoc' -DarkMode -DefinitionTag 'v3.1' -EndpointName 'endpoint_v3.1' - Enable-PodeOAViewer -Type RapiDoc -Path '/docs/rapidoc' -DarkMode -DefinitionTag 'v3.1' -EndpointName 'endpoint_v3.1' - Enable-PodeOAViewer -Type StopLight -Path '/docs/stoplight' -DarkMode -DefinitionTag 'v3.1' -EndpointName 'endpoint_v3.1' - Enable-PodeOAViewer -Type Explorer -Path '/docs/explorer' -DarkMode -DefinitionTag 'v3.1' -EndpointName 'endpoint_v3.1' - Enable-PodeOAViewer -Bookmarks -Path '/docs' -DefinitionTag 'v3.1' -EndpointName 'endpoint_v3.1' + Enable-PodeOAViewer -Type Swagger -Path '/docs/swagger' -DefinitionTag 'v3.1' + Enable-PodeOAViewer -Type ReDoc -Path '/docs/vredoc' -DarkMode -DefinitionTag 'v3.1' + Enable-PodeOAViewer -Type RapiDoc -Path '/docs/rapidoc' -DarkMode -DefinitionTag 'v3.1' + Enable-PodeOAViewer -Type StopLight -Path '/docs/stoplight' -DarkMode -DefinitionTag 'v3.1' + Enable-PodeOAViewer -Type Explorer -Path '/docs/explorer' -DarkMode -DefinitionTag 'v3.1' + Enable-PodeOAViewer -Bookmarks -Path '/docs' -DefinitionTag 'v3.1' # setup session details Enable-PodeSessionMiddleware -Duration 120 -Extend @@ -258,10 +295,10 @@ Some useful links: Add-PodeOAComponentSchema -Name 'ApiResponse' - New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Pet' | Add-PodeOAComponentRequestBody -Name 'Pet' -Description 'Pet object that needs to be added to the store' + New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet' | Add-PodeOAComponentRequestBody -Name 'Pet' -Description 'Pet object that needs to be added to the store' Add-PodeOAComponentRequestBody -Name 'UserArray' -Description 'List of user object' -Content ( - New-PodeOAContentMediaType -MediaType 'application/json' -Content 'User' -Array) + New-PodeOAContentMediaType -ContentType 'application/json' -Content 'User' -Array) @@ -307,9 +344,9 @@ Some useful links: } | Set-PodeOARouteInfo -Summary 'Update an existing pet' -Description 'Update an existing pet by Id' -Tags 'pet' -OperationId 'updatePet' -PassThru | Set-PodeOARequest -RequestBody ( New-PodeOARequestBody -Description 'Update an existent pet in the store' -Required -Content ( - New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Pet' ) + New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet' ) ) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Pet' ) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet' ) -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' -PassThru | Add-PodeOAResponse -StatusCode 404 -Description 'Pet not found' -PassThru | Add-PodeOAResponse -StatusCode 405 -Description 'Validation exception' @@ -345,9 +382,9 @@ Some useful links: } } | Set-PodeOARouteInfo -Summary 'Add a new pet to the store' -Description 'Add a new pet to the store' -Tags 'pet' -OperationId 'addPet' -PassThru | Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Description 'Create a new pet in the store' -Required -Content ( - New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Pet' ) + New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet' ) ) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Pet' ) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet' ) -PassThru | Add-PodeOAResponse -StatusCode 405 -Description 'Invalid input' @@ -376,7 +413,7 @@ Some useful links: Set-PodeOARequest -PassThru -Parameters ( New-PodeOAStringProperty -Name 'status' -Description 'Status values that need to be considered for filter' -Default 'available' -Enum @('available', 'pending', 'sold') | ConvertTo-PodeOAParameter -In Query -Explode ) | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Pet' -Array) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet' -Array) -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid status value' -PassThru | Add-PodeOAResponse -StatusCode 415 @@ -404,7 +441,7 @@ Some useful links: Set-PodeOARequest -PassThru -Parameters ( New-PodeOAStringProperty -Name 'tags' -Description 'Tags to filter by' -Array | ConvertTo-PodeOAParameter -In Query -Explode ) | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Pet' -Array) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet' -Array) -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid tag value' -PassThru | Add-PodeOAResponse -StatusCode 415 @@ -437,7 +474,7 @@ Some useful links: Set-PodeOARequest -PassThru -Parameters ( New-PodeOAIntProperty -Name 'petId' -Description 'ID of pet to return' -Format Int64 | ConvertTo-PodeOAParameter -In Path -Required ) | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Pet') -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet') -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' -PassThru | Add-PodeOAResponse -StatusCode 404 -Description 'Pet not found' -PassThru | Add-PodeOAResponse -StatusCode 415 @@ -514,7 +551,7 @@ Some useful links: ( New-PodeOAIntProperty -Name 'petId' -Format Int64 -Description 'ID of pet to update' -Required | ConvertTo-PodeOAParameter -In Path ), ( New-PodeOAStringProperty -Name 'additionalMetadata' -Description 'Additional Metadata' | ConvertTo-PodeOAParameter -In Query ) ) -RequestBody ( - New-PodeOARequestBody -Content ( New-PodeOAContentMediaType -MediaType 'application/octet-stream' -Upload ) + New-PodeOARequestBody -Content ( New-PodeOAContentMediaType -ContentType 'application/octet-stream' -Upload ) ) -PassThru | Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{'application/json' = 'ApiResponse' } @@ -562,7 +599,7 @@ Some useful links: Write-PodeHtmlResponse -StatusCode 405 -Value ($Validate.message -join ', ') } } | Set-PodeOARouteInfo -Summary 'Place an order for a pet' -Description 'Place a new order in the store' -Tags 'store' -OperationId 'placeOrder' -PassThru | - Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'Order' )) -PassThru | + Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'Order' )) -PassThru | Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (@{ 'application/json' = 'Order' }) -PassThru | Add-PodeOAResponse -StatusCode 405 -Description 'Invalid Input' @@ -592,7 +629,7 @@ Some useful links: Set-PodeOARequest -PassThru -Parameters @( ( New-PodeOAIntProperty -Name 'orderId' -Format Int64 -Description 'ID of order that needs to be fetched' -Required | ConvertTo-PodeOAParameter -In Path ) ) | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'Order' ) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Order' ) -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' -PassThru | Add-PodeOAResponse -StatusCode 404 -Description 'Order not found' @@ -660,9 +697,9 @@ Some useful links: Write-PodeHtmlResponse -StatusCode 405 -Value ($Validate.message -join ', ') } } | Set-PodeOARouteInfo -Summary 'Create user.' -Description 'This can only be done by the logged in user.' -Tags 'user' -OperationId 'createUser' -PassThru | - Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'User' )) -PassThru | + Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'User' )) -PassThru | Add-PodeOAResponse -StatusCode 405 -Description 'Invalid Input' -PassThru | - Add-PodeOAResponse -Default -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'User' ) + Add-PodeOAResponse -Default -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'User' ) <# @@ -706,8 +743,8 @@ Some useful links: default { Write-PodeHtmlResponse -StatusCode 415 } } } | Set-PodeOARouteInfo -Summary 'Creates list of users with given input array.' -Description 'Creates list of users with given input array.' -Tags 'user' -OperationId 'createUsersWithListInput' -PassThru | - Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Content (New-PodeOAContentMediaType -MediaType 'application/json' -Content 'User' -Array)) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'User' -Array ) -PassThru | + Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Content (New-PodeOAContentMediaType -ContentType 'application/json' -Content 'User' -Array)) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'User' -Array ) -PassThru | Add-PodeOAResponse -Default -Description 'successful operation' @@ -740,7 +777,7 @@ Some useful links: } | Set-PodeOARouteInfo -Summary 'Logs user into the system.' -Tags 'user' -OperationId 'loginUser' -PassThru | Set-PodeOARequest -Parameters ( New-PodeOAStringProperty -Name 'username' -Description 'The user name for login' | ConvertTo-PodeOAParameter -In Query ), ( New-PodeOAStringProperty -Name 'password' -Description 'The password for login in clear text' -Format Password | ConvertTo-PodeOAParameter -In Query ) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'string' ) ` + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'string' ) ` -Headers (New-PodeOAIntProperty -Name 'X-Rate-Limit' -Description 'calls per hour allowed by the user' -Format Int32), (New-PodeOAStringProperty -Name 'X-Expires-After' -Description 'date in UTC when token expires' -Format Date-Time) -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid username/password supplied' @@ -778,7 +815,7 @@ Some useful links: } } | Set-PodeOARouteInfo -Summary 'Get user by user name' -Tags 'user' -OperationId 'getUserByName' -PassThru | Set-PodeOARequest -Parameters ( New-PodeOAStringProperty -Name 'username' -Description 'The name that needs to be fetched. Use user1 for testing.' -Required | ConvertTo-PodeOAParameter -In Path ) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Content (New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml' -Content 'User' ) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'User' ) -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid username supplied' -PassThru | Add-PodeOAResponse -StatusCode 404 -Description 'User not found' @@ -827,7 +864,7 @@ Some useful links: } | Set-PodeOARouteInfo -Summary 'Update user' -Description 'This can only be done by the logged in user.' -Tags 'user' -OperationId 'updateUser' -PassThru | Set-PodeOARequest -Parameters ( New-PodeOAStringProperty -Name 'username' -Description ' name that need to be updated.' -Required | ConvertTo-PodeOAParameter -In Path ) ` -RequestBody ( New-PodeOARequestBody -Required -Description 'Update an existent user in the store' -Content ( - New-PodeOAContentMediaType -MediaType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'User' + New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'User' )) -PassThru | Add-PodeOAResponse -StatusCode 405 -Description 'Invalid Input' -PassThru | Add-PodeOAResponse -StatusCode 404 -Description 'User not found' -PassThru | @@ -851,21 +888,19 @@ Some useful links: Write-PodeJsonReWrite-PodeHtmlResponsesponse -Value 'Invalid username supplied' -StatusCode 400 } } | Set-PodeOARouteInfo -Summary 'Delete user' -Description 'This can only be done by the logged in user.' -Tags 'user' -OperationId 'deleteUser' -PassThru | - Set-PodeOARequest -Parameters ( New-PodeOAStringProperty -Name 'username' -Description 'The name that needs to be deleted.' -Required | ConvertTo-PodeOAParameter -In Path ) -PassThru | + Set-PodeOARequest -Parameters ( New-PodeOAStringProperty -Name 'username' -Description 'The name that needs to be deleted.' -Required | ConvertTo-PodeOAParameter -In Path ) -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid username supplied' -PassThru | Add-PodeOAResponse -StatusCode 404 -Description 'User not found' } } - $yaml = get-PodeOADefinition -Format Yaml -DefinitionTag 'v3.0.3' - - $yaml_31 = get-PodeOADefinition -Format Yaml -DefinitionTag 'v3.1' - - - - #$r= ConvertFrom-PodeXml -node $xmlDoc + $yaml_30 = Get-PodeOADefinition -Format Yaml -DefinitionTag 'v3.0.3' - #$pet=$r |convertto-json + $yaml_31 = Get-PodeOADefinition -Format Yaml -DefinitionTag 'v3.1' - #$Validate = Test-PodeOAJsonSchemaCompliance -Json $pet -SchemaReference 'Pet' - # $json= PodeOADefinition -Format Json + Write-PodeHost 'OpenAPI v3.0.3' + Write-PodeHost $yaml_30 + Write-PodeHost '####################################' + Write-PodeHost 'OpenAPI v3.1.0' + Write-PodeHost $yaml_31 + Write-PodeHost '####################################' } \ No newline at end of file diff --git a/examples/Schedules-CronHelper.ps1 b/examples/Schedules-CronHelper.ps1 new file mode 100644 index 000000000..b6fc6f179 --- /dev/null +++ b/examples/Schedules-CronHelper.ps1 @@ -0,0 +1,53 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with HTTP endpoints and a scheduled task. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081 with a scheduled task that runs every 2 minutes. + It includes an endpoint for GET requests. + +.EXAMPLE + To run the sample: ./Schedules-CronHelper.ps1 + + Invoke-RestMethod -Uri http://localhost:8081 -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Schedules-CronHelper.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +Start-PodeServer { + + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + $cron = New-PodeCron -Every Minute -Interval 2 + Add-PodeSchedule -Name 'example' -Cron $cron -ScriptBlock { + 'Hi there!' | Out-Default + } + + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Write-PodeJsonResponse -Value @{ Result = 1 } + } + +} \ No newline at end of file diff --git a/examples/Schedules-LongRunning.ps1 b/examples/Schedules-LongRunning.ps1 new file mode 100644 index 000000000..b82e8b4c4 --- /dev/null +++ b/examples/Schedules-LongRunning.ps1 @@ -0,0 +1,57 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with multiple scheduled tasks. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081 with multiple scheduled tasks. Each task runs every minute + and sleeps for a random duration between 5 and 40 seconds. The maximum concurrency for schedules is set to 30. + +.EXAMPLE + To run the sample: ./Schedules-LongRunning.ps1 + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Schedules-LongRunning.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + # add lots of schedules that each sleep for a while + 1..30 | ForEach-Object { + Add-PodeSchedule -Name "Schedule_$($_)" -Cron '@minutely' -ArgumentList @{ ID = $_ } -ScriptBlock { + param($ID) + + $seconds = (Get-Random -Minimum 5 -Maximum 40) + Start-Sleep -Seconds $seconds + "ID: $($ID) [$($seconds)]" | Out-PodeHost + } + } + + Set-PodeScheduleConcurrency -Maximum 30 + +} \ No newline at end of file diff --git a/examples/Schedules-Routes.ps1 b/examples/Schedules-Routes.ps1 new file mode 100644 index 000000000..9196d0c01 --- /dev/null +++ b/examples/Schedules-Routes.ps1 @@ -0,0 +1,53 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with dynamic schedule creation. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081 with the ability to create new schedules dynamically via an API route. + The server is configured with schedule pooling enabled, and includes an endpoint to create a new schedule that runs every minute. + +.EXAMPLE + To run the sample: ./Schedules-Routes.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/api/schedule -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Schedules-Routes.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer -EnablePool Schedules { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + # create a new schdule via a route + Add-PodeRoute -Method Get -Path '/api/schedule' -ScriptBlock { + Add-PodeSchedule -Name 'example' -Cron '@minutely' -ScriptBlock { + 'hello there' | out-default + } + } + +} diff --git a/examples/schedules.ps1 b/examples/Schedules.ps1 similarity index 54% rename from examples/schedules.ps1 rename to examples/Schedules.ps1 index ce15ba42b..0bf017bdd 100644 --- a/examples/schedules.ps1 +++ b/examples/Schedules.ps1 @@ -1,14 +1,48 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with various scheduled tasks. +.DESCRIPTION + This script sets up a Pode server listening on port 8081 with multiple schedules configured. + It demonstrates scheduling using predefined cron expressions, schedules from files, + multiple cron expressions, and scheduling at specific times. + Additionally, it includes a route to invoke a schedule's logic ad-hoc with arguments. + +.EXAMPLE + To run the sample: ./Schedules.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/api/run -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Schedules.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } # or just: # Import-Module Pode -# create a server, and start listening on port 8085 +# create a server, and start listening on port 8081 Start-PodeServer { - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http # schedule minutely using predefined cron $message = 'Hello, world!' diff --git a/examples/ServerFrom-File.ps1 b/examples/ServerFrom-File.ps1 new file mode 100644 index 000000000..a61425064 --- /dev/null +++ b/examples/ServerFrom-File.ps1 @@ -0,0 +1,38 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a basic Pode server. + +.DESCRIPTION + This script sets up a Pode server using a server definition from an external script file. The server listens on port 8081, logs errors to the terminal, uses the Pode view engine, and includes a timer and a route for HTTP GET requests. + +.EXAMPLE + To run the sample: ./ServerFrom-File.ps1 + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/ServerFrom-File.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a basic server +Start-PodeServer -FilePath "$ScriptPath/scripts/server.ps1" -CurrentPath \ No newline at end of file diff --git a/examples/shared-state.ps1 b/examples/Shared-State.ps1 similarity index 58% rename from examples/shared-state.ps1 rename to examples/Shared-State.ps1 index 7f67a38df..a62f0e687 100644 --- a/examples/shared-state.ps1 +++ b/examples/Shared-State.ps1 @@ -1,5 +1,39 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with state management and logging. + +.DESCRIPTION + This script sets up a Pode server that listens on port 8081, logs requests and errors to the terminal, and manages state using timers and routes. The server initializes state from a JSON file, updates state periodically using timers, and provides routes to interact with the state. + +.EXAMPLE + To run the sample: ./Shared-State.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/array -Method Get + Invoke-RestMethod -Uri http://localhost:8081/array3 -Method Get + Invoke-RestMethod -Uri http://localhost:8081/array -Method Delete + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Shared-State.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } # or just: # Import-Module Pode @@ -7,7 +41,7 @@ Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop # create a basic server Start-PodeServer { - Add-PodeEndpoint -Address * -Port 8090 -Protocol Http + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging diff --git a/examples/SwaggerEditor/Swagger-Editor.ps1 b/examples/SwaggerEditor/Swagger-Editor.ps1 new file mode 100644 index 000000000..175345172 --- /dev/null +++ b/examples/SwaggerEditor/Swagger-Editor.ps1 @@ -0,0 +1,60 @@ +<# +.SYNOPSIS + Sets up a Pode server to serve the Swagger editor and static files. + +.DESCRIPTION + This script configures a Pode server to listen on a specified port (default is 8081) and serve the Swagger editor + and other static files. It enables request and error logging and sets up a view engine for rendering HTML. + +.PARAMETER Port + The port number on which the Pode server will listen. Default is 8081. + +.EXAMPLE + To run the sample: ./SwaggerEditor/Swagger-Editor.ps1 + + Use a browser to access http://127.0.0.1:8081/ + + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/SwaggerEditor/Swagger-Editor.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +param( + [int] + $Port = 8081 +) + +try { + $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)) + $podePath = Split-Path -Parent -Path $ScriptPath + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port $port -Protocol Http + New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + # set view engine to pode renderer + Set-PodeViewEngine -Type HTML + + # STATIC asset folder route + Add-PodeStaticRoute -Path '/editor' -Source './www' -Defaults @('index.html') -FileBrowser + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Move-PodeResponseUrl -Url '/editor/index.html' + } +} \ No newline at end of file diff --git a/examples/SwaggerEditor/swagger-editor.ps1 b/examples/SwaggerEditor/swagger-editor.ps1 deleted file mode 100644 index 1a5d17d0b..000000000 --- a/examples/SwaggerEditor/swagger-editor.ps1 +++ /dev/null @@ -1,29 +0,0 @@ -param( - [int] - $Port = 8080 -) - -$path = Split-Path -Parent -Path (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening on port 8085 -Start-PodeServer -Threads 2 { - - # listen on localhost:8085 - Add-PodeEndpoint -Address localhost -Port $port -Protocol Http - New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - # set view engine to pode renderer - Set-PodeViewEngine -Type HTML - - # STATIC asset folder route - Add-PodeStaticRoute -Path '/editor/swagger-editor-dist' -Source "$($path)/src/Misc/swagger-editor-dist" -FileBrowser - Add-PodeStaticRoute -Path '/editor' -Source './www' -Defaults @('index.html') -FileBrowser - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - Move-PodeResponseUrl -Url '/editor/index.html' - } -} \ No newline at end of file diff --git a/examples/Tasks.ps1 b/examples/Tasks.ps1 new file mode 100644 index 000000000..9b98e730a --- /dev/null +++ b/examples/Tasks.ps1 @@ -0,0 +1,76 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with task management and logging. + +.DESCRIPTION + This script sets up a Pode server that listens on port 8081, logs errors to the terminal, and handles both synchronous and asynchronous tasks. The server provides routes to interact with the tasks and demonstrates task invocation and waiting. + +.EXAMPLE + To run the sample: ./Tasks.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/api/task/sync -Method Get + Invoke-RestMethod -Uri http://localhost:8081/api/task/sync2 -Method Get + Invoke-RestMethod -Uri http://localhost:8081/api/task/async -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Tasks.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a basic server +Start-PodeServer { + + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + Add-PodeTask -Name 'Test1' -ScriptBlock { + 'a string' + 4 + return @{ InnerValue = 'hey look, a value!' } + } + + Add-PodeTask -Name 'Test2' -ScriptBlock { + param($value) + Start-Sleep -Seconds 10 + "a $($value) is never late, it arrives exactly when it means to" | Out-Default + } + + # create a new timer via a route + Add-PodeRoute -Method Get -Path '/api/task/sync' -ScriptBlock { + $result = Invoke-PodeTask -Name 'Test1' -Wait + Write-PodeJsonResponse -Value @{ Result = $result } + } + + Add-PodeRoute -Method Get -Path '/api/task/sync2' -ScriptBlock { + $task = Invoke-PodeTask -Name 'Test1' + $result = ($task | Wait-PodeTask) + Write-PodeJsonResponse -Value @{ Result = $result } + } + + Add-PodeRoute -Method Get -Path '/api/task/async' -ScriptBlock { + Invoke-PodeTask -Name 'Test2' -ArgumentList @{ value = 'wizard' } | Out-Null + Write-PodeJsonResponse -Value @{ Result = 'jobs done' } + } + +} diff --git a/examples/Tcp-Server.ps1 b/examples/Tcp-Server.ps1 new file mode 100644 index 000000000..4d21b0c95 --- /dev/null +++ b/examples/Tcp-Server.ps1 @@ -0,0 +1,84 @@ +<# +.SYNOPSIS + A PowerShell script to set up a Pode TCP server with multiple endpoints and error logging. + +.DESCRIPTION + This script sets up a Pode TCP server that listens on multiple endpoints, logs errors to the terminal, and handles incoming TCP requests with specific verbs. + The server provides handlers for 'HELLO', 'HELLO2', 'HELLO3', 'QUIT', and a catch-all handler for unrecognized verbs. + It also demonstrates reading client input and responding accordingly. + +.EXAMPLE + To run the sample: ./Tcp-Server.ps1 + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Tcp-Server.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # add two endpoints + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Tcp -CRLFMessageEnd #-Acknowledge 'Welcome!' + # Add-PodeEndpoint -Address localhost -Port 9000 -Protocol Tcps -SelfSigned -CRLFMessageEnd -TlsMode Explicit -Acknowledge 'Welcome!' + # Add-PodeEndpoint -Address localhost -Port 9000 -Protocol Tcps -SelfSigned -CRLFMessageEnd -TlsMode Implicit -Acknowledge 'Welcome!' + + # enable logging + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + Add-PodeVerb -Verb 'HELLO' -ScriptBlock { + Write-PodeTcpClient -Message "HI" + 'here' | Out-Default + } + + Add-PodeVerb -Verb 'HELLO2 :username' -ScriptBlock { + Write-PodeTcpClient -Message "HI2, $($TcpEvent.Parameters.username)" + } + + Add-PodeVerb -Verb * -ScriptBlock { + Write-PodeTcpClient -Message 'Unrecognised verb sent' + } + + # Add-PodeVerb -Verb * -Close -ScriptBlock { + # $TcpEvent.Request.Body | Out-Default + # Write-PodeTcpClient -Message "HTTP/1.1 200 `r`nConnection: close`r`n`r`nHello, there" + # } + + # Add-PodeVerb -Verb 'STARTTLS' -UpgradeToSsl + + # Add-PodeVerb -Verb 'STARTTLS' -ScriptBlock { + # Write-PodeTcpClient -Message 'TLS GO AHEAD' + # $TcpEvent.Request.UpgradeToSSL() + # } + + # Add-PodeVerb -Verb 'QUIT' -Close + + Add-PodeVerb -Verb 'QUIT' -ScriptBlock { + Write-PodeTcpClient -Message 'Bye!' + Close-PodeTcpClient + } + + Add-PodeVerb -Verb 'HELLO3' -ScriptBlock { + Write-PodeTcpClient -Message "Hi! What's your name?" + $name = Read-PodeTcpClient -CRLFMessageEnd + Write-PodeTcpClient -Message "Hi, $($name)!" + } +} \ No newline at end of file diff --git a/examples/Tcp-ServerAuth.ps1 b/examples/Tcp-ServerAuth.ps1 new file mode 100644 index 000000000..4d3733e21 --- /dev/null +++ b/examples/Tcp-ServerAuth.ps1 @@ -0,0 +1,66 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode TCP server with role-based access control and logging. + +.DESCRIPTION + This script sets up a Pode TCP server that listens on port 8081, logs errors to the terminal, and implements role-based access control. The server provides an endpoint that restricts access based on user roles retrieved from a database. + +.EXAMPLE + To run the sample: ./Tcp-ServerAuth.ps1 + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Tcp-ServerAuth.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # add endpoint + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Tcp -CRLFMessageEnd + + # enable logging + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + # create a role access method get retrieves roles from a database + New-PodeAccessScheme -Type Role | Add-PodeAccess -Name 'RoleExample' -ScriptBlock { + param($username) + if ($username -ieq 'morty') { + return @('Developer') + } + + return 'QA' + } + + # setup a Verb that only allows Developers + Add-PodeVerb -Verb 'EXAMPLE :username' -ScriptBlock { + if (!(Test-PodeAccess -Name 'RoleExample' -Destination 'Developer' -ArgumentList $TcpEvent.Parameters.username)) { + Write-PodeTcpClient -Message 'Forbidden Access' + 'Forbidden!' | Out-Default + return + } + + Write-PodeTcpClient -Message 'Hello, there!' + 'Hello!' | Out-Default + } +} \ No newline at end of file diff --git a/examples/Tcp-ServerHttp.ps1 b/examples/Tcp-ServerHttp.ps1 new file mode 100644 index 000000000..4e99f537e --- /dev/null +++ b/examples/Tcp-ServerHttp.ps1 @@ -0,0 +1,52 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode TCP server with multiple endpoints and error logging. + +.DESCRIPTION + This script sets up a Pode TCP server that listens on port 8081, logs errors to the terminal, and handles incoming HTTP requests. The server provides a catch-all handler for HTTP requests and returns a basic HTML response. + +.EXAMPLE + To run the sample: ./Tcp-ServerHttp.ps1 + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Tcp-ServerHttp.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # add two endpoints + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Tcp + + # enable logging + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + # catch-all for http + Add-PodeVerb -Verb '*' -Close -ScriptBlock { + $TcpEvent.Request.Body | Out-Default + Write-PodeTcpClient -Message "HTTP/1.1 200 `r`nConnection: close`r`n`r`nHello, there" + # navigate to "http://localhost:8081" + } + +} \ No newline at end of file diff --git a/examples/Tcp-ServerMultiEndpoint.ps1 b/examples/Tcp-ServerMultiEndpoint.ps1 new file mode 100644 index 000000000..f624a74dd --- /dev/null +++ b/examples/Tcp-ServerMultiEndpoint.ps1 @@ -0,0 +1,66 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode TCP server with multiple endpoints and error logging. + +.DESCRIPTION + This script sets up a Pode TCP server that listens on multiple endpoints, logs errors to the terminal, and handles incoming TCP requests with specific verbs. The server provides handlers for 'HELLO' and 'Quit' verbs and a catch-all handler for unrecognized verbs. + +.EXAMPLE + To run the sample: ./Tcp-ServerMultiEndpoint.ps1 + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Tcp-ServerMultiEndpoint.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # add two endpoints + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Tcp -Name 'EP1' -Acknowledge 'Hello there!' -CRLFMessageEnd + Add-PodeEndpoint -Address localhost -Hostname 'foo.pode.com' -Port 9000 -Protocol Tcp -Name 'EP2' -Acknowledge 'Hello there!' -CRLFMessageEnd + + # enable logging + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + # hello verb for endpoint1 + Add-PodeVerb -Verb 'HELLO :forename :surname' -EndpointName EP1 -ScriptBlock { + Write-PodeTcpClient -Message "HI 1, $($TcpEvent.Parameters.forename) $($TcpEvent.Parameters.surname)" + "HI 1, $($TcpEvent.Parameters.forename) $($TcpEvent.Parameters.surname)" | Out-Default + } + + # hello verb for endpoint2 + Add-PodeVerb -Verb 'HELLO :forename :surname' -EndpointName EP2 -ScriptBlock { + Write-PodeTcpClient -Message "HI 2, $($TcpEvent.Parameters.forename) $($TcpEvent.Parameters.surname)" + "HI 2, $($TcpEvent.Parameters.forename) $($TcpEvent.Parameters.surname)" | Out-Default + } + + # catch-all verb for both endpoints + Add-PodeVerb -Verb '*' -ScriptBlock { + Write-PodeTcpClient -Message "Unrecognised verb sent" + } + + # quit verb for both endpoints + Add-PodeVerb -Verb 'Quit' -Close + +} \ No newline at end of file diff --git a/examples/threading.ps1 b/examples/Threading.ps1 similarity index 61% rename from examples/threading.ps1 rename to examples/Threading.ps1 index 6d7f700ae..22cbff47e 100644 --- a/examples/threading.ps1 +++ b/examples/Threading.ps1 @@ -1,11 +1,61 @@ +<# +.SYNOPSIS + A PowerShell script to set up a Pode server with various lock mechanisms including custom locks, mutexes, and semaphores. + +.DESCRIPTION + This script sets up a Pode server that listens on a specified port and demonstrates the usage of lockables, mutexes, and semaphores for thread synchronization. + It includes routes that showcase the behavior of these synchronization mechanisms in different scopes (self, local, and global). + The server provides multiple routes to test custom locks, mutexes, and semaphores by simulating delays and concurrent access. + +.EXAMPLE + To run the sample: ./Threading.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/lock/custom/route1 -Method Get + Invoke-RestMethod -Uri http://localhost:8081/lock/custom/route2 -Method Get + Invoke-RestMethod -Uri http://localhost:8081/lock/global/route1 -Method Get + Invoke-RestMethod -Uri http://localhost:8081/lock/global/route2 -Method Get + Invoke-RestMethod -Uri http://localhost:8081/mutex/self/route1 -Method Get + Invoke-RestMethod -Uri http://localhost:8081/mutex/self/route2 -Method Get + Invoke-RestMethod -Uri http://localhost:8081/mutex/local/route1 -Method Get + Invoke-RestMethod -Uri http://localhost:8081/mutex/local/route2 -Method Get + Invoke-RestMethod -Uri http://localhost:8081/mutex/global/route1 -Method Get + Invoke-RestMethod -Uri http://localhost:8081/mutex/global/route2 -Method Get + Invoke-RestMethod -Uri http://localhost:8081/semaphore/self/route1 -Method Get + Invoke-RestMethod -Uri http://localhost:8081/semaphore/self/route2 -Method Get + Invoke-RestMethod -Uri http://localhost:8081/semaphore/local/route1 -Method Get + Invoke-RestMethod -Uri http://localhost:8081/semaphore/local/route2 -Method Get + Invoke-RestMethod -Uri http://localhost:8081/semaphore/global/route1 -Method Get + Invoke-RestMethod -Uri http://localhost:8081/semaphore/global/route2 -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Threading.ps1 + +.PARAMETER Port + The port number on which the Pode server will listen. Default is 8081. + +.NOTES + Author: Pode Team + License: MIT License +#> param( [Parameter()] [int] - $Port = 8090 + $Port = 8081 ) +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } # or just: # Import-Module Pode @@ -16,10 +66,9 @@ Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop Start-PodeServer -Threads 2 { - Add-PodeEndpoint -Address * -Port $Port -Protocol Http + Add-PodeEndpoint -Address localhost -Port $Port -Protocol Http New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - # custom locks New-PodeLockable -Name 'TestLock' @@ -99,7 +148,6 @@ Start-PodeServer -Threads 2 { Write-PodeJsonResponse -Value @{ Route = 2; Thread = $ThreadId } } - # self semaphore New-PodeSemaphore -Name 'SelfSemaphore' diff --git a/examples/Timers-Route.ps1 b/examples/Timers-Route.ps1 new file mode 100644 index 000000000..2efcbe649 --- /dev/null +++ b/examples/Timers-Route.ps1 @@ -0,0 +1,54 @@ +<# +.SYNOPSIS + A PowerShell script to set up a basic Pode server with timer functionality. + +.DESCRIPTION + This script sets up a Pode server that listens on a specified port and allows the creation of timers via HTTP routes. + It includes a route to create a new timer that runs a specified script block at defined intervals. + +.EXAMPLE + To run the sample: ./Timers-Route.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/api/timer -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Timers-Route.ps1 + +.PARAMETER Port + The port number on which the Pode server will listen. Default is 8081. + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a basic server +Start-PodeServer -EnablePool Timers { + + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + # create a new timer via a route + Add-PodeRoute -Method Get -Path '/api/timer' -ScriptBlock { + Add-PodeTimer -Name 'example' -Interval 5 -ScriptBlock { + 'hello there' | out-default + } + } + +} diff --git a/examples/timers.ps1 b/examples/Timers.ps1 similarity index 56% rename from examples/timers.ps1 rename to examples/Timers.ps1 index 091b98d56..dc590012e 100644 --- a/examples/timers.ps1 +++ b/examples/Timers.ps1 @@ -1,5 +1,41 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +<# +.SYNOPSIS + A PowerShell script to set up a Pode server with various timer configurations. + +.DESCRIPTION + This script sets up a Pode server that listens on a specified port and creates multiple timers with different behaviors. + It includes routes to create new timers via HTTP requests and invoke existing timers on demand. + +.EXAMPLE + To run the sample: ./Timers.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/api/timer -Method Get + Invoke-RestMethod -Uri http://localhost:8081/api/run -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Timers.ps1 + +.PARAMETER Port + The port number on which the Pode server will listen. Default is 8081. + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } # or just: # Import-Module Pode @@ -7,7 +43,7 @@ Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop # create a basic server Start-PodeServer { - Add-PodeEndpoint -Address * -Port 8081 -Protocol Http + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http # runs forever, looping every 5secs $message = 'Hello, world' @@ -30,12 +66,14 @@ Start-PodeServer { # runs forever, but skips the first 3 "loops" - is paused for 15secs then loops every 5secs Add-PodeTimer -Name 'pause-first-3' -Interval 5 -ScriptBlock { 'Skip 3 then run' | Out-PodeHost + Write-PodeHost $TimerEvent -Explode -ShowType } -Skip 3 # runs every 5secs, but only runs for 3 "loops" (ie, 15secs) Add-PodeTimer -Name 'run-3-times' -Interval 5 -ScriptBlock { 'Only run 3 times' | Out-PodeHost Get-PodeTimer -Name 'run-3-times' | Out-Default + Write-PodeHost $TimerEvent -Explode -ShowType } -Limit 3 # skip the first 2 loops, then run for 15 loops @@ -46,6 +84,7 @@ Start-PodeServer { # run once after 2mins Add-PodeTimer -Name 'run-once' -Interval 20 -ScriptBlock { 'Ran once' | Out-PodeHost + Write-PodeHost $TimerEvent -Explode -ShowType } -Skip 1 -Limit 1 # create a new timer via a route diff --git a/examples/Web-AuthApiKey.ps1 b/examples/Web-AuthApiKey.ps1 new file mode 100644 index 000000000..6addc1971 --- /dev/null +++ b/examples/Web-AuthApiKey.ps1 @@ -0,0 +1,96 @@ +<# +.SYNOPSIS + A PowerShell script to set up a Pode server with API key authentication and various route configurations. + +.DESCRIPTION + This script sets up a Pode server that listens on a specified port, enables request and error logging, + and configures API key authentication. It also defines a route to fetch a list of users, requiring authentication. + +.PARAMETER Location + The location where the API key is expected. Valid values are 'Header', 'Query', and 'Cookie'. Default is 'Header'. + +.EXAMPLE + To run the sample: ./Web-AuthApiKey.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/users -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthApiKey.ps1 + +.NOTES + Use: + Invoke-RestMethod -Method Get -Uri 'http://localhost:8081/users' -Headers @{ 'X-API-KEY' = 'test-api-key' } + +.NOTES + Author: Pode Team + License: MIT License +#> +param( + [Parameter()] + [ValidateSet('Header', 'Query', 'Cookie')] + [string] + $Location = 'Header' +) + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + New-PodeLoggingMethod -File -Name 'requests' | Enable-PodeRequestLogging + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + # setup bearer auth + New-PodeAuthScheme -ApiKey -Location $Location | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { + param($key) + + # here you'd check a real user storage, this is just for example + if ($key -ieq 'test-api-key') { + return @{ + User = @{ + ID = 'M0R7Y302' + Name = 'Morty' + Type = 'Human' + } + } + } + + return $null + } + + # GET request to get list of users (since there's no session, authentication will always happen) + Add-PodeRoute -Method Get -Path '/users' -Authentication 'Validate' -ScriptBlock { + Write-PodeJsonResponse -Value @{ + Users = @( + @{ + Name = 'Deep Thought' + Age = 42 + }, + @{ + Name = 'Leeroy Jenkins' + Age = 1337 + } + ) + } + } + +} \ No newline at end of file diff --git a/examples/Web-AuthBasic.ps1 b/examples/Web-AuthBasic.ps1 new file mode 100644 index 000000000..25815359a --- /dev/null +++ b/examples/Web-AuthBasic.ps1 @@ -0,0 +1,85 @@ +<# +.SYNOPSIS + A PowerShell script to set up a Pode server with sessionless Basic authentication for REST APIs. + +.DESCRIPTION + This script sets up a Pode server that listens on a specified port, enables sessionless Basic authentication, + and provides an endpoint to get user information. + +.EXAMPLE + To run the sample: ./Web-AuthBasic.ps1 + + This example shows how to use sessionless authentication, which will mostly be for + REST APIs. The example used here is Basic authentication. + + Calling the '[POST] http://localhost:8081/users' endpoint, with an Authorization + header of 'Basic bW9ydHk6cGlja2xl' will display the uesrs. Anything else and + you'll get a 401 status code back. + + Success: + Invoke-RestMethod -Uri http://localhost:8081/users -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' } + + Failure: + Invoke-RestMethod -Uri http://localhost:8081/users -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cmljaw==' } + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthBasic.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + # request logging + New-PodeLoggingMethod -Terminal -Batch 10 -BatchTimeout 10 | Enable-PodeRequestLogging + + # setup basic auth (base64> username:password in header) + New-PodeAuthScheme -Basic -Realm 'Pode Example Page' | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { + param($username, $password) + + # here you'd check a real user storage, this is just for example + if ($username -eq 'morty' -and $password -eq 'pickle') { + return @{ + User = @{ + Username = 'morty' + ID = 'M0R7Y302' + Name = 'Morty' + Type = 'Human' + } + } + } + + return @{ Message = 'Invalid details supplied' } + } + + # POST request to get current user (since there's no session, authentication will always happen) + Add-PodeRoute -Method Post -Path '/users' -Authentication 'Validate' -ScriptBlock { + Write-PodeJsonResponse -Value @{ + User = (Get-PodeAuthUser) + } + } + +} \ No newline at end of file diff --git a/examples/web-auth-basic-access.ps1 b/examples/Web-AuthBasicAccess.ps1 similarity index 66% rename from examples/web-auth-basic-access.ps1 rename to examples/Web-AuthBasicAccess.ps1 index 11959566e..1bd7d36c5 100644 --- a/examples/web-auth-basic-access.ps1 +++ b/examples/Web-AuthBasicAccess.ps1 @@ -1,29 +1,61 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +<# +.SYNOPSIS + A PowerShell script to set up a Pode server with basic authentication and role/group-based access control. -# or just: -# Import-Module Pode +.DESCRIPTION + This script sets up a Pode server that listens on a specified port, enables request and error logging, + configures basic authentication, and sets up role and group-based access control. It defines various routes + with specific access requirements. -<# -This example shows how to use sessionless authentication, which will mostly be for -REST APIs. The example used here is Basic authentication. +.PARAMETER Location + The location where the API key is expected. Valid values are 'Header', 'Query', and 'Cookie'. Default is 'Header'. + +.EXAMPLE + To run the sample: ./Web-AuthBasicAccess.ps1 + + This example shows how to use sessionless authentication, which will mostly be for + REST APIs. The example used here is Basic authentication. + + Calling the '[POST] http://localhost:8081/users-all' endpoint, with an Authorization + header of 'Basic bW9ydHk6cGlja2xl' will display the users. Anything else and + you'll get a 401 status code back. -Calling the '[POST] http://localhost:8085/users-all' endpoint, with an Authorization -header of 'Basic bW9ydHk6cGlja2xl' will display the uesrs. Anything else and -you'll get a 401 status code back. + Success: + Invoke-RestMethod -Uri http://localhost:8081/users-all -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' } -Success: -Invoke-RestMethod -Uri http://localhost:8085/users-all -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' } + Failure: + Invoke-RestMethod -Uri http://localhost:8081/users-all -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cmljaw==' } -Failure: -Invoke-RestMethod -Uri http://localhost:8085/users-all -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cmljaw==' } +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Dot-SourceScript.ps1 + +.NOTES + Author: Pode Team + License: MIT License #> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode -# create a server, and start listening on port 8085 +# create a server, and start listening on port 8081 Start-PodeServer -Threads 2 { - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http # setup RBAC New-PodeAccessScheme -Type Role | Add-PodeAccess -Name 'TestRbac' diff --git a/examples/Web-AuthBasicAdhoc.ps1 b/examples/Web-AuthBasicAdhoc.ps1 new file mode 100644 index 000000000..c4eea0b73 --- /dev/null +++ b/examples/Web-AuthBasicAdhoc.ps1 @@ -0,0 +1,99 @@ +<# +.SYNOPSIS + A PowerShell script to set up a Pode server with adhoc Basic authentication for REST APIs. + +.DESCRIPTION + This script sets up a Pode server that listens on a specified port, enables basic authentication + for a REST API endpoint, and returns user information upon successful authentication. The authentication + details are checked on a per-request basis without using session-based authentication. + +.PARAMETER Location + The location where the API key is expected. Valid values are 'Header', 'Query', and 'Cookie'. Default is 'Header'. + +.EXAMPLE + To run the sample: ./Web-AuthBasicAdhoc.ps1 + + This example shows how to use sessionless authentication, which will mostly be for + REST APIs. The example used here is adhoc Basic authentication. + + Calling the '[POST] http://localhost:8081/users' endpoint, with an Authorization + header of 'Basic bW9ydHk6cGlja2xl' will display the users. Anything else and + you'll get a 401 status code back. + + Success: + Invoke-RestMethod -Uri http://localhost:8081/users -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' } + + Failure: + Invoke-RestMethod -Uri http://localhost:8081/users -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cmljaw==' } + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthBasicAdhoc.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + # setup basic auth (base64> username:password in header) + New-PodeAuthScheme -Basic -Realm 'Pode Example Page' | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { + param($username, $password) + + # here you'd check a real user storage, this is just for example + if ($username -eq 'morty' -and $password -eq 'pickle') { + return @{ + User = @{ + ID ='M0R7Y302' + Name = 'Morty' + Type = 'Human' + } + } + } + + return @{ Message = 'Invalid details supplied' } + } + + # POST request to get list of users (authentication is done adhoc, and not directly using -Authentication on the Route) + Add-PodeRoute -Method Post -Path '/users' -ScriptBlock { + if (!(Test-PodeAuth -Name Validate)) { + Set-PodeResponseStatus -Code 401 + return + } + + Write-PodeJsonResponse -Value @{ + User = @( + @{ + Name = 'Deep Thought' + Age = 42 + }, + @{ + Name = 'Leeroy Jenkins' + Age = 1337 + } + ) + } + } + +} \ No newline at end of file diff --git a/examples/Web-AuthBasicAnon.ps1 b/examples/Web-AuthBasicAnon.ps1 new file mode 100644 index 000000000..597c3cf9c --- /dev/null +++ b/examples/Web-AuthBasicAnon.ps1 @@ -0,0 +1,106 @@ +<# +.SYNOPSIS + A PowerShell script to set up a Pode server with Basic authentication for REST APIs. + +.DESCRIPTION + This script sets up a Pode server that listens on a specified port, enables basic authentication + for a REST API endpoint, and returns user information upon successful authentication. The authentication + details are checked on a per-request basis without using session-based authentication. + +.PARAMETER Location + The location where the API key is expected. Valid values are 'Header', 'Query', and 'Cookie'. Default is 'Header'. + +.EXAMPLE + To run the sample: ./Web-AuthBasicAnon.ps1 + + This example shows how to use sessionless authentication, which will mostly be for + REST APIs. The example used here is Basic authentication. + + Calling the '[POST] http://localhost:8081/users' endpoint, with an Authorization + header of 'Basic bW9ydHk6cGlja2xl' will display the uesrs. Anything else and + you'll get a 401 status code back. + + Success: + Invoke-RestMethod -Uri http://localhost:8081/users -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' } + + Failure: + Invoke-RestMethod -Uri http://localhost:8081/users -Headers @{ Authorization = 'Basic bW9ydHk6cmljaw==' } + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthBasicAnon.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + # setup basic auth (base64> username:password in header) + New-PodeAuthScheme -Basic -Realm 'Pode Example Page' | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { + param($username, $password) + + # here you'd check a real user storage, this is just for example + if ($username -eq 'morty' -and $password -eq 'pickle') { + return @{ + User = @{ + ID ='M0R7Y302' + Name = 'Morty' + Type = 'Human' + } + } + } + + return @{ Message = 'Invalid details supplied' } + } + + # GET request to get list of users (since there's no session, authentication will always happen, but, we're allowing anon access) + Add-PodeRoute -Method Get -Path '/users' -Authentication 'Validate' -AllowAnon -ScriptBlock { + if (Test-PodeAuthUser) { + Write-PodeJsonResponse -Value @{ + Users = @( + @{ + Name = 'Deep Thought' + Age = 42 + }, + @{ + Name = 'Leeroy Jenkins' + Age = 1337 + } + ) + } + } + else { + Write-PodeJsonResponse -Value @{ + Users = @( + @{ + Name = 'John Smith' + Age = 21 + } + ) + } + } + } + +} \ No newline at end of file diff --git a/examples/Web-AuthBasicBearer.ps1 b/examples/Web-AuthBasicBearer.ps1 new file mode 100644 index 000000000..f6c73820d --- /dev/null +++ b/examples/Web-AuthBasicBearer.ps1 @@ -0,0 +1,82 @@ +<# +.SYNOPSIS + A PowerShell script to set up a Pode server with sessionless Bearer authentication for REST APIs. + +.DESCRIPTION + This script sets up a Pode server that listens on a specified port, enables sessionless Bearer authentication, + and provides an endpoint to get a list of users. + +.EXAMPLE + To run the sample: ./Web-AuthBasicBearer.ps1 + + Invoke-RestMethod -Method Get -Uri 'http://localhost:8081/users' -Headers @{ Authorization = 'Bearer test-token' } + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthBasicBearer.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + New-PodeLoggingMethod -File -Name 'requests' | Enable-PodeRequestLogging + + # setup bearer auth + New-PodeAuthScheme -Bearer -Scope write | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { + param($token) + + # here you'd check a real user storage, this is just for example + if ($token -ieq 'test-token') { + return @{ + User = @{ + ID ='M0R7Y302' + Name = 'Morty' + Type = 'Human' + } + Scope = 'write' + } + } + + return $null + } + + # GET request to get list of users (since there's no session, authentication will always happen) + Add-PodeRoute -Method Get -Path '/users' -Authentication 'Validate' -ScriptBlock { + Write-PodeJsonResponse -Value @{ + Users = @( + @{ + Name = 'Deep Thought' + Age = 42 + }, + @{ + Name = 'Leeroy Jenkins' + Age = 1337 + } + ) + } + } + +} \ No newline at end of file diff --git a/examples/web-auth-clientcert.ps1 b/examples/Web-AuthBasicClientcert.ps1 similarity index 53% rename from examples/web-auth-clientcert.ps1 rename to examples/Web-AuthBasicClientcert.ps1 index 3b76551a7..489b37b72 100644 --- a/examples/web-auth-clientcert.ps1 +++ b/examples/Web-AuthBasicClientcert.ps1 @@ -1,5 +1,35 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +<# +.SYNOPSIS + PowerShell script to set up a Pode server with HTTPS and client certificate authentication. + +.DESCRIPTION + This script sets up a Pode server that listens on a specified port with HTTPS using a self-signed certificate. + It enables client certificate authentication for securing access to the server. + + .EXAMPLE + To run the sample: ./Web-AuthBasicClientcert.ps1 + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthBasicClientcert.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } # or just: # Import-Module Pode @@ -8,7 +38,7 @@ Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop Start-PodeServer { # bind to ip/port and set as https with self-signed cert - Add-PodeEndpoint -Address * -Port 8443 -Protocol Https -SelfSigned -AllowClientCertificate + Add-PodeEndpoint -Address localhost -Port 8443 -Protocol Https -SelfSigned -AllowClientCertificate # set view engine for web pages Set-PodeViewEngine -Type Pode diff --git a/examples/Web-AuthBasicHeader.ps1 b/examples/Web-AuthBasicHeader.ps1 new file mode 100644 index 000000000..0fb0f8d27 --- /dev/null +++ b/examples/Web-AuthBasicHeader.ps1 @@ -0,0 +1,105 @@ +<# +.SYNOPSIS + A PowerShell script to set up a Pode server with session-based Basic authentication for REST APIs. + +.DESCRIPTION + This script sets up a Pode server that listens on a specified port, enables session-based authentication + using headers, and provides login and logout functionality. Authenticated users can access a REST API endpoint + to retrieve user information. + +.PARAMETER Location + The location where the API key is expected. Valid values are 'Header', 'Query', and 'Cookie'. Default is 'Header'. + +.EXAMPLE + To run the sample: ./Web-AuthBasicHeader.ps1 + + This example shows how to use session authentication on REST APIs using Headers. + The example used here is Basic authentication. + + Login: + $session = (Invoke-WebRequest -Uri http://localhost:8081/login -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' }).Headers['pode.sid'] + + Users: + Invoke-RestMethod -Uri http://localhost:8081/users -Method Post -Headers @{ 'pode.sid' = "$session" } + + Logout: + Invoke-WebRequest -Uri http://localhost:8081/logout -Method Post -Headers @{ 'pode.sid' = "$session" } + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthBasicHeader.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + # enable error logging + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + # setup session details + Enable-PodeSessionMiddleware -Duration 120 -Extend -UseHeaders -Strict + + # setup basic auth (base64> username:password in header) + New-PodeAuthScheme -Basic | Add-PodeAuth -Name 'Login' -ScriptBlock { + param($username, $password) + + # here you'd check a real user storage, this is just for example + if ($username -eq 'morty' -and $password -eq 'pickle') { + return @{ + User = @{ + ID ='M0R7Y302' + Name = 'Morty' + Type = 'Human' + } + } + } + + return @{ Message = 'Invalid details supplied' } + } + + # POST request to login + Add-PodeRoute -Method Post -Path '/login' -Authentication 'Login' + + # POST request to logout + Add-PodeRoute -Method Post -Path '/logout' -Authentication 'Login' -Logout + + # POST request to get list of users - the "pode.sid" header is expected + Add-PodeRoute -Method Post -Path '/users' -Authentication 'Login' -ScriptBlock { + Write-PodeJsonResponse -Value @{ + Users = @( + @{ + Name = 'Deep Thought' + Age = 42 + }, + @{ + Name = 'Leeroy Jenkins' + Age = 1337 + } + ) + } + } + +} \ No newline at end of file diff --git a/examples/Web-AuthDigest.ps1 b/examples/Web-AuthDigest.ps1 new file mode 100644 index 000000000..7c26eef53 --- /dev/null +++ b/examples/Web-AuthDigest.ps1 @@ -0,0 +1,80 @@ +<# +.SYNOPSIS + PowerShell script to set up a Pode server with Digest authentication. + +.DESCRIPTION + This script sets up a Pode server that listens on a specified port and uses Digest authentication + for securing access to the server. The authentication details are checked against predefined user data. + +.EXAMPLE + To run the sample: ./Web-AuthDigest.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/users -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthDigest.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + # setup digest auth + New-PodeAuthScheme -Digest | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { + param($username, $params) + + # here you'd check a real user storage, this is just for example + if ($username -ieq 'morty') { + return @{ + User = @{ + ID ='M0R7Y302' + Name = 'Morty' + Type = 'Human' + } + Password = 'pickle' + } + } + + return $null + } + + # GET request to get list of users (since there's no session, authentication will always happen) + Add-PodeRoute -Method Get -Path '/users' -Authentication 'Validate' -ScriptBlock { + Write-PodeJsonResponse -Value @{ + Users = @( + @{ + Name = 'Deep Thought' + Age = 42 + }, + @{ + Name = 'Leeroy Jenkins' + Age = 1337 + } + ) + } + } + +} \ No newline at end of file diff --git a/examples/web-auth-form.ps1 b/examples/Web-AuthForm.ps1 similarity index 57% rename from examples/web-auth-form.ps1 rename to examples/Web-AuthForm.ps1 index e9c5e57f9..45fad7a19 100644 --- a/examples/web-auth-form.ps1 +++ b/examples/Web-AuthForm.ps1 @@ -1,24 +1,57 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +<# +.SYNOPSIS + Sample script demonstrating session persistent authentication using Pode. -# or just: -# Import-Module Pode +.DESCRIPTION + This script sets up a Pode server that listens on localhost:8081 and uses session-based authentication + for user logins. The authentication is demonstrated using a simple form where users can log in with + predefined credentials (username: morty, password: pickle). Upon successful login, users are greeted + on the home page, and the view counter is incremented. Users can log out, which will purge the session + and redirect them to the login page. -<# -This examples shows how to use session persistant authentication, for things like logins on websites. -The example used here is Form authentication, sent from the in HTML. +.PARAMETER ScriptPath + Path of the script being executed. + +.PARAMETER podePath + Path of the Pode module. + +.EXAMPLE + To run the sample: ./Web-AuthForm.ps1 + + Run this script to start the Pode server and navigate to 'http://localhost:8081' in your browser. + You will be redirected to the login page, where you can log in with the credentials provided above. + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthForm.ps1 -Navigating to the 'http://localhost:8085' endpoint in your browser will auto-rediect you to the '/login' -page. Here, you can type the username (morty) and the password (pickle); clicking 'Login' will take you -back to the home page with a greeting and a view counter. Clicking 'Logout' will purge the session and -take you back to the login page. +.NOTES + Author: Pode Team + License: MIT License #> -# create a server, and start listening on port 8085 +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 Start-PodeServer -Threads 2 { - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http # set the view engine Set-PodeViewEngine -Type Pode diff --git a/examples/web-auth-form-access.ps1 b/examples/Web-AuthFormAccess.ps1 similarity index 59% rename from examples/web-auth-form-access.ps1 rename to examples/Web-AuthFormAccess.ps1 index de372faa3..3bf19c479 100644 --- a/examples/web-auth-form-access.ps1 +++ b/examples/Web-AuthFormAccess.ps1 @@ -1,30 +1,54 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -if (Test-Path -Path "$($path)/src/Pode.psm1" -PathType Leaf) { - Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop -} -else { - Import-Module -Name 'Pode' -} - <# -This examples shows how to use session persistant authentication with access. -The example used here is Form authentication and RBAC access on pages, sent from the in HTML. +.SYNOPSIS + PowerShell script to set up a Pode server with Form authentication and RBAC access. + +.DESCRIPTION + This script sets up a Pode server that listens on a specified port and uses Form authentication + for securing access to different pages. Role-based access control (RBAC) is also implemented + to restrict access to certain pages based on user roles. + +.EXAMPLE + To run the sample: ./Web-AuthFormAccess.ps1 + + This examples shows how to use session persistant authentication with access. + The example used here is Form authentication and RBAC access on pages, sent from the in HTML. -Navigating to the 'http://localhost:8085' endpoint in your browser will auto-rediect you to the '/login' -page. Here, you can type the username (morty) and the password (pickle); clicking 'Login' will take you -back to the home page with a greeting and a view counter. Clicking 'Logout' will purge the session and -take you back to the login page. + Navigating to the 'http://localhost:8081' endpoint in your browser will auto-rediect you to the '/login' + page. Here, you can type the username (morty) and the password (pickle); clicking 'Login' will take you + back to the home page with a greeting and a view counter. Clicking 'Logout' will purge the session and + take you back to the login page. -- The Home and Login pages are accessible by all. -- The About page is only accessible by Developers (for morty it will load) -- The Register page is only accessible by QAs (for morty this will 403) + - The Home and Login pages are accessible by all. + - The About page is only accessible by Developers (for morty it will load) + - The Register page is only accessible by QAs (for morty this will 403) + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthFormAccess.ps1 + +.NOTES + Author: Pode Team + License: MIT License #> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } -# create a server, and start listening on port 8085 +# create a server, and start listening on port 8081 Start-PodeServer -Threads 2 { - # listen on localhost:8085 - Add-PodeEndpoint -Address localhost -Port 8085 -Protocol Http + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http # set the view engine Set-PodeViewEngine -Type Pode diff --git a/examples/web-auth-form-ad.ps1 b/examples/Web-AuthFormAd.ps1 similarity index 51% rename from examples/web-auth-form-ad.ps1 rename to examples/Web-AuthFormAd.ps1 index f91e73a93..3864de8c1 100644 --- a/examples/web-auth-form-ad.ps1 +++ b/examples/Web-AuthFormAd.ps1 @@ -1,24 +1,53 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +<# +.SYNOPSIS + PowerShell script to set up a Pode server with Form authentication using Windows Active Directory. -# or just: -# Import-Module Pode +.DESCRIPTION + This script sets up a Pode server that listens on a specified port and uses Form authentication + with Windows Active Directory for securing access to different pages. The home page view counter + is stored in the session data, which is persisted across user sessions. -<# -This examples shows how to use session persistant authentication using Windows Active Directory. -The example used here is Form authentication, sent from the in HTML. +.EXAMPLE + To run the sample: ./Web-AuthFormAd.ps1 + + This examples shows how to use session persistant authentication using Windows Active Directory. + The example used here is Form authentication, sent from the in HTML. + + Navigating to the 'http://localhost:8081' endpoint in your browser will auto-rediect you to the '/login' + page. Here, you can type the details for a domain user. Clicking 'Login' will take you back to the home + page with a greeting and a view counter. Clicking 'Logout' will purge the session and take you back to + the login page. -Navigating to the 'http://localhost:8085' endpoint in your browser will auto-rediect you to the '/login' -page. Here, you can type the details for a domain user. Clicking 'Login' will take you back to the home -page with a greeting and a view counter. Clicking 'Logout' will purge the session and take you back to -the login page. +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthFormAd.ps1 + +.NOTES + Author: Pode Team + License: MIT License #> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode -# create a server, and start listening on port 8085 +# create a server, and start listening on port 8081 Start-PodeServer -Threads 2 { - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging # set the view engine diff --git a/examples/web-auth-form-anon.ps1 b/examples/Web-AuthFormAnon.ps1 similarity index 57% rename from examples/web-auth-form-anon.ps1 rename to examples/Web-AuthFormAnon.ps1 index 18d25c151..3860d5ed1 100644 --- a/examples/web-auth-form-anon.ps1 +++ b/examples/Web-AuthFormAnon.ps1 @@ -1,24 +1,58 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with session persistent authentication for a login system. -# or just: -# Import-Module Pode +.DESCRIPTION + This script sets up a Pode server listening on port 8081 with session persistent authentication. -<# -This examples shows how to use session persistant authentication, for things like logins on websites. -The example used here is Form authentication, sent from the in HTML. +.EXAMPLE + To run the sample: ./Web-AuthFormAnon.ps1 + + + This examples shows how to use session persistant authentication, for things like logins on websites. + The example used here is Form authentication, sent from the in HTML. + + Navigating to the 'http://localhost:8081' endpoint in your browser will auto-rediect you to the '/login' + page. Here, you can type the username (morty) and the password (pickle); clicking 'Login' will take you + back to the home page with a greeting and a view counter. Clicking 'Logout' will purge the session and + take you back to the login page. + + With Authentication + Invoke-RestMethod -Uri http://localhost:8081/ -Method Get -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' } -Navigating to the 'http://localhost:8085' endpoint in your browser will auto-rediect you to the '/login' -page. Here, you can type the username (morty) and the password (pickle); clicking 'Login' will take you -back to the home page with a greeting and a view counter. Clicking 'Logout' will purge the session and -take you back to the login page. + Anonymous + Invoke-RestMethod -Uri http://localhost:8081/ -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthFormAnon.ps1 + +.NOTES + Author: Pode Team + License: MIT License #> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode -# create a server, and start listening on port 8085 +# create a server, and start listening on port 8081 Start-PodeServer -Threads 2 { - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http # set the view engine Set-PodeViewEngine -Type Pode diff --git a/examples/web-auth-form-creds.ps1 b/examples/Web-AuthFormCreds.ps1 similarity index 56% rename from examples/web-auth-form-creds.ps1 rename to examples/Web-AuthFormCreds.ps1 index aac22042c..282d7a2bc 100644 --- a/examples/web-auth-form-creds.ps1 +++ b/examples/Web-AuthFormCreds.ps1 @@ -1,25 +1,58 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with session persistent authentication for a login system. -# or just: -# Import-Module Pode +.DESCRIPTION + This script sets up a Pode server listening on port 8081 with session persistent authentication. + It demonstrates a login system using form authentication, converting credentials into a pscredential object. -<# -This examples shows how to use session persistant authentication, for things like logins on websites. -The example used here is Form authentication, sent from the in HTML, and converts the -credentials into a pscredential object - -Navigating to the 'http://localhost:8085' endpoint in your browser will auto-rediect you to the '/login' -page. Here, you can type the username (morty) and the password (pickle); clicking 'Login' will take you -back to the home page with a greeting and a view counter. Clicking 'Logout' will purge the session and -take you back to the login page. +.EXAMPLE + To run the sample: ./Web-AuthFormCreds.ps1 + + This examples shows how to use session persistant authentication, for things like logins on websites. + The example used here is Form authentication, sent from the in HTML, and converts the + credentials into a pscredential object + + Navigating to the 'http://localhost:8081' endpoint in your browser will auto-rediect you to the '/login' + page. Here, you can type the username (morty) and the password (pickle); clicking 'Login' will take you + back to the home page with a greeting and a view counter. Clicking 'Logout' will purge the session and + take you back to the login page. + + # Login url + http://localhost:8081/login + #logout url + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthFormCreds.ps1 + +.NOTES + Author: Pode Team + License: MIT License #> -# create a server, and start listening on port 8085 +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 Start-PodeServer -Threads 2 { - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http # set the view engine Set-PodeViewEngine -Type Pode diff --git a/examples/Web-AuthFormFile.ps1 b/examples/Web-AuthFormFile.ps1 new file mode 100644 index 000000000..70f55c320 --- /dev/null +++ b/examples/Web-AuthFormFile.ps1 @@ -0,0 +1,101 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with session persistent authentication using a user file. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081 with session persistent authentication. + It demonstrates a login system using form authentication against a user file. + +.EXAMPLE + To run the sample: ./Web-AuthFormFile.ps1 + + This examples shows how to use session persistant authentication using a user file. + The example used here is Form authentication, sent from the in HTML. + + Navigating to the 'http://localhost:8081' endpoint in your browser will auto-rediect you to the '/login' + page. Here, you can type the details for a user in the json file. Clicking 'Login' will take you back to the home + page with a greeting and a view counter. Clicking 'Logout' will purge the session and take you back to + the login page. + + username = r.sanchez + password = pickle + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthFormFile.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + # set the view engine + Set-PodeViewEngine -Type Pode + + # setup session details + Enable-PodeSessionMiddleware -Duration 120 -Extend + + # setup form auth against user file ( in HTML) + New-PodeAuthScheme -Form | Add-PodeAuthUserFile -Name 'Login' -FilePath './users/users.json' -FailureUrl '/login' -SuccessUrl '/' -ScriptBlock { + param($user) + return @{ User = $user } + } + + + # home page: + # redirects to login page if not authenticated + Add-PodeRoute -Method Get -Path '/' -Authentication Login -ScriptBlock { + $WebEvent.Session.Data.Views++ + $session:TestData | Out-Default + + Write-PodeViewResponse -Path 'auth-home' -Data @{ + Username = $WebEvent.Auth.User.Name + Views = $WebEvent.Session.Data.Views + } + } + + + # login page: + # the login flag set below checks if there is already an authenticated session cookie. If there is, then + # the user is redirected to the home page. If there is no session then the login page will load without + # checking user authetication (to prevent a 401 status) + Add-PodeRoute -Method Get -Path '/login' -Authentication Login -Login -ScriptBlock { + Write-PodeViewResponse -Path 'auth-login' -FlashMessages + } + + + # login check: + # this is the endpoint the 's action will invoke. If the user validates then they are set against + # the session as authenticated, and redirect to the home page. If they fail, then the login page reloads + Add-PodeRoute -Method Post -Path '/login' -Authentication Login -Login + + + # logout check: + # when the logout button is click, this endpoint is invoked. The logout flag set below informs this call + # to purge the currently authenticated session, and then redirect back to the login page + Add-PodeRoute -Method Post -Path '/logout' -Authentication Login -Logout +} \ No newline at end of file diff --git a/examples/web-auth-form-local.ps1 b/examples/Web-AuthFormLocal.ps1 similarity index 52% rename from examples/web-auth-form-local.ps1 rename to examples/Web-AuthFormLocal.ps1 index 066c40fa3..3c55cafbd 100644 --- a/examples/web-auth-form-local.ps1 +++ b/examples/Web-AuthFormLocal.ps1 @@ -1,24 +1,51 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with session persistent authentication using Windows Local users. -# or just: -# Import-Module Pode +.DESCRIPTION + This script sets up a Pode server listening on port 8081 with session persistent authentication. + It demonstrates a login system using form authentication against Windows Local users. -<# -This examples shows how to use session persistant authentication using Windows Local users. -The example used here is Form authentication, sent from the in HTML. +.EXAMPLE + To run the sample: ./Web-AuthFormLocal.ps1 + + This examples shows how to use session persistant authentication using Windows Local users. + The example used here is Form authentication, sent from the in HTML. + + Navigating to 'http://localhost:8081' in your browser will redirect you to the '/login' page. + You can log in using the details for a domain user. After logging in, you will see a greeting and a view counter. + Clicking 'Logout' will purge the session and take you back to the login page. -Navigating to the 'http://localhost:8085' endpoint in your browser will auto-rediect you to the '/login' -page. Here, you can type the details for a domain user. Clicking 'Login' will take you back to the home -page with a greeting and a view counter. Clicking 'Logout' will purge the session and take you back to -the login page. +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthFormLocal.ps1 + +.NOTES + Author: Pode Team + License: MIT License #> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode -# create a server, and start listening on port 8085 +# create a server, and start listening on port 8081 Start-PodeServer -Threads 2 { - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging # set the view engine diff --git a/examples/web-auth-form-merged.ps1 b/examples/Web-AuthFormMerged.ps1 similarity index 63% rename from examples/web-auth-form-merged.ps1 rename to examples/Web-AuthFormMerged.ps1 index 9f180fbcf..8345b7101 100644 --- a/examples/web-auth-form-merged.ps1 +++ b/examples/Web-AuthFormMerged.ps1 @@ -1,24 +1,52 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with session persistent authentication for logins on websites. -# or just: -# Import-Module Pode +.DESCRIPTION + This script sets up a Pode server listening on port 8081 with session persistent authentication. + It demonstrates a login system using form authentication. -<# -This examples shows how to use session persistant authentication, for things like logins on websites. -The example used here is Form authentication, sent from the in HTML. +.EXAMPLE + To run the sample: ./Web-AuthFormMerged.ps1 + + This examples shows how to use session persistant authentication, for things like logins on websites. + The example used here is Form authentication, sent from the in HTML. + + Navigating to the 'http://localhost:8081' endpoint in your browser will auto-rediect you to the '/login' + page. Here, you can type the username (morty) and the password (pickle); clicking 'Login' will take you + back to the home page with a greeting and a view counter. Clicking 'Logout' will purge the session and + take you back to the login page. -Navigating to the 'http://localhost:8085' endpoint in your browser will auto-rediect you to the '/login' -page. Here, you can type the username (morty) and the password (pickle); clicking 'Login' will take you -back to the home page with a greeting and a view counter. Clicking 'Logout' will purge the session and -take you back to the login page. +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthFormMerged.ps1 + +.NOTES + Author: Pode Team + License: MIT License #> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode -# create a server, and start listening on port 8085 +# create a server, and start listening on port 8081 Start-PodeServer -Threads 2 { - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http # set the view engine Set-PodeViewEngine -Type Pode diff --git a/examples/web-auth-form-session-auth.ps1 b/examples/Web-AuthFormSessionAuth.ps1 similarity index 56% rename from examples/web-auth-form-session-auth.ps1 rename to examples/Web-AuthFormSessionAuth.ps1 index aee830686..6545ad9bb 100644 --- a/examples/web-auth-form-session-auth.ps1 +++ b/examples/Web-AuthFormSessionAuth.ps1 @@ -1,25 +1,54 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with session persistent authentication for logins on websites. -# or just: -# Import-Module Pode +.DESCRIPTION + This script sets up a Pode server listening on port 8081 with session persistent authentication. + It demonstrates a login system using form authentication and session authentication on the main home page route and form authentication on login. -<# -This examples shows how to use session persistant authentication, for things like logins on websites. -The example used here is Form authentication, sent from the in HTML. But also used is Session Authentication -on the main home page route and Form Auth on Login. - -Navigating to the 'http://localhost:8085' endpoint in your browser will auto-rediect you to the '/login' -page. Here, you can type the username (morty) and the password (pickle); clicking 'Login' will take you -back to the home page with a greeting and a view counter. Clicking 'Logout' will purge the session and -take you back to the login page. +.EXAMPLE + To run the sample: ./Web-AuthFormSessionAuth.ps1 + + This examples shows how to use session persistant authentication, for things like logins on websites. + The example used here is Form authentication, sent from the in HTML. But also used is Session Authentication + on the main home page route and Form Auth on Login. + + Navigating to the 'http://localhost:8081' endpoint in your browser will auto-rediect you to the '/login' + page. Here, you can type the username (morty) and the password (pickle); clicking 'Login' will take you + back to the home page with a greeting and a view counter. Clicking 'Logout' will purge the session and + take you back to the login page. + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthFormSessionAuth.ps1 + +.NOTES + Author: Pode Team + License: MIT License #> -# create a server, and start listening on port 8085 +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 Start-PodeServer -Threads 2 { - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http # set the view engine Set-PodeViewEngine -Type Pode diff --git a/examples/web-auth-merged.ps1 b/examples/Web-AuthMerged.ps1 similarity index 62% rename from examples/web-auth-merged.ps1 rename to examples/Web-AuthMerged.ps1 index 46992c209..c349487c3 100644 --- a/examples/web-auth-merged.ps1 +++ b/examples/Web-AuthMerged.ps1 @@ -1,20 +1,51 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with combined API key and Basic authentication, as well as role and group-based access control. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081. It demonstrates how to use both API key and Basic authentication, + combined together for access control based on roles and groups. The script includes routes that require authentication and + specific roles and groups to access. + +.EXAMPLE + To run the sample: ./Web-AuthMerged.ps1 + + Success: + Invoke-RestMethod -Method Get -Uri 'http://localhost:8081/users' -Headers @{ 'X-API-KEY' = 'test-api-key'; Authorization = 'Basic bW9ydHk6cGlja2xl' } + + Failure: + Invoke-RestMethod -Method Get -Uri 'http://localhost:8081/users' -Headers @{ 'X-API-KEY' = 'test-api-key'; Authorization = 'Basic bW9ydHk6cmljaw==' + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthMerged.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } # or just: # Import-Module Pode -# Success -# Invoke-RestMethod -Method Get -Uri 'http://localhost:8085/users' -Headers @{ 'X-API-KEY' = 'test-api-key'; Authorization = 'Basic bW9ydHk6cGlja2xl' } - -# Failure -# Invoke-RestMethod -Method Get -Uri 'http://localhost:8085/users' -Headers @{ 'X-API-KEY' = 'test-api-key'; Authorization = 'Basic bW9ydHk6cmljaw==' } - -# create a server, and start listening on port 8085 +# create a server, and start listening on port 8081 Start-PodeServer -Threads 2 { - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging # request logging diff --git a/examples/Web-AuthOauth2.ps1 b/examples/Web-AuthOauth2.ps1 new file mode 100644 index 000000000..befdf7f68 --- /dev/null +++ b/examples/Web-AuthOauth2.ps1 @@ -0,0 +1,87 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with session persistent authentication using Azure AD and OAuth2. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081. It demonstrates how to use session persistent authentication + with Azure AD and OAuth2. + +.EXAMPLE + To run the sample: ./Web-AuthOauth2.ps1 + + Navigating to the 'http://localhost:8081' endpoint in your browser will auto-rediect you to Azure. + There, login to Azure and you'll be redirected back to the home page + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthOauth2.ps1 + +.NOTES + Author: Pode Team + License: MIT License + + Important!!! You'll need to register a new app in Azure, and note your clientId, secret, and tenant in the variables below. +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http -Default + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + # set the view engine + Set-PodeViewEngine -Type Pode + + # setup session details + Enable-PodeSessionMiddleware -Duration 120 -Extend + + # setup auth against Azure AD (the following are from registering an app in the portal) + $clientId = '' + $clientSecret = '' + $tenantId = '' + + $scheme = New-PodeAuthAzureADScheme -Tenant $tenantId -ClientId $clientId -ClientSecret $clientSecret + $scheme | Add-PodeAuth -Name 'Login' -FailureUrl '/login' -SuccessUrl '/' -ScriptBlock { + param($user, $accessToken, $refreshToken) + return @{ User = $user } + } + + + # home page: + # redirects to login page if not authenticated + Add-PodeRoute -Method Get -Path '/' -Authentication Login -ScriptBlock { + $WebEvent.Session.Data.Views++ + + Write-PodeViewResponse -Path 'auth-home' -Data @{ + Username = $WebEvent.Auth.User.name + Views = $WebEvent.Session.Data.Views + } + } + + + # login - this will just redirect to azure + Add-PodeRoute -Method Get -Path '/login' -Authentication Login + + + # logout check: + # when the logout button is click, this endpoint is invoked. The logout flag set below informs this call + # to purge the currently authenticated session, and then redirect back to the login page + Add-PodeRoute -Method Post -Path '/logout' -Authentication Login -Logout +} \ No newline at end of file diff --git a/examples/web-auth-oauth2-form.ps1 b/examples/Web-AuthOauth2Form.ps1 similarity index 52% rename from examples/web-auth-oauth2-form.ps1 rename to examples/Web-AuthOauth2Form.ps1 index 6cbc1baa6..a2f546c3a 100644 --- a/examples/web-auth-oauth2-form.ps1 +++ b/examples/Web-AuthOauth2Form.ps1 @@ -1,24 +1,49 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with session persistent authentication using Azure AD and OAuth2. -# or just: -# Import-Module Pode +.DESCRIPTION + This script sets up a Pode server listening on port 8081. It demonstrates how to use session persistent authentication + with Azure AD and OAuth2, using a form for login without redirection. -<# -This examples shows how to use session persistant authentication using Azure AD and OAuth2, using a Form with no redirecting. +.EXAMPLE + To run the sample: ./Web-AuthFormCreds.ps1 + + Navigating to the 'http://localhost:8081' endpoint in your browser will auto-rediect you to the /login form. + There, enter you Azure AD email/password, Pode with authenticate and then take you to the home page + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthFormCreds.ps1 -Navigating to the 'http://localhost:8085' endpoint in your browser will auto-rediect you to the /login form. -There, enter you Azure AD email/password, Pode with authenticate and then take you to the home page +.NOTES + Author: Pode Team + License: MIT License -Note: You'll need to register a new app in Azure, and note you clientId, secret, and tenant - in the variables below. + Important!!! You'll need to register a new app in Azure, and note your clientId, secret, and tenant in the variables below. #> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode -# create a server, and start listening on port 8085 +# create a server, and start listening on port 8081 Start-PodeServer -Threads 2 { - # listen on localhost:8085 - Add-PodeEndpoint -Address localhost -Port 8085 -Protocol Http -Default + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http -Default New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging # set the view engine @@ -47,7 +72,7 @@ Start-PodeServer -Threads 2 { Write-PodeViewResponse -Path 'auth-home' -Data @{ Username = $WebEvent.Auth.User.name - Views = $WebEvent.Session.Data.Views + Views = $WebEvent.Session.Data.Views } } diff --git a/examples/web-auth-oauth2-oidc.ps1 b/examples/Web-AuthOauth2Oidc.ps1 similarity index 50% rename from examples/web-auth-oauth2-oidc.ps1 rename to examples/Web-AuthOauth2Oidc.ps1 index ea2cbc6d4..c090be8da 100644 --- a/examples/web-auth-oauth2-oidc.ps1 +++ b/examples/Web-AuthOauth2Oidc.ps1 @@ -1,24 +1,49 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with session persistent authentication using Google Cloud and OpenID Connect Discovery. -# or just: -# Import-Module Pode +.DESCRIPTION + This script sets up a Pode server listening on port 8081. It demonstrates how to use session persistent authentication + with Google Cloud and OpenID Connect Discovery. -<# -This examples shows how to use session persistant authentication using Google Cloud and OpenID Connect Discovery +.EXAMPLE + To run the sample: ./Web-AuthOauth2Oidc.ps1 + + Navigating to the 'http://localhost:8081' endpoint in your browser will auto-rediect you to Google. + There, login to Google account and you'll be redirected back to the home page -Navigating to the 'http://localhost:8085' endpoint in your browser will auto-rediect you to Google. -There, login to Google account and you'll be redirected back to the home page +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthOauth2Oidc.ps1 + +.NOTES + Author: Pode Team + License: MIT License -Note: You'll need to register a new project/app in Google Cloud, and note your clientId and secret - in the variables below. + Important!!! You'll need to register a new project/app in Google Cloud, and note your clientId and secret in the variables below. #> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode -# create a server, and start listening on port 8085 +# create a server, and start listening on port 8081 Start-PodeServer -Threads 2 { - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http -Default + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http -Default New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging # set the view engine diff --git a/examples/web-cookies.ps1 b/examples/Web-Cookies.ps1 similarity index 50% rename from examples/web-cookies.ps1 rename to examples/Web-Cookies.ps1 index a7393ff3c..f89c9f73f 100644 --- a/examples/web-cookies.ps1 +++ b/examples/Web-Cookies.ps1 @@ -1,14 +1,48 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with cookie management and signed cookies. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081. It includes routes for setting, extending, + removing, and checking signed cookies. The server uses a global cookie secret for signing the cookies. + +.EXAMPLE + To run the sample: ./Web-Cookies.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/ -Method Get + Invoke-RestMethod -Uri http://localhost:8081/remove -Method Get + Invoke-RestMethod -Uri http://localhost:8081/check -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-Cookies.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } # or just: # Import-Module Pode -# create a server, and start listening on port 8090 +# create a server, and start listening on port 8081 Start-PodeServer -Threads 2 { - # listen on localhost:8090 - Add-PodeEndpoint -Address localhost -Port 8090 -Protocol Http + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http # set view engine to pode renderer Set-PodeViewEngine -Type HTML diff --git a/examples/Web-Csrf.ps1 b/examples/Web-Csrf.ps1 new file mode 100644 index 000000000..ae7860be9 --- /dev/null +++ b/examples/Web-Csrf.ps1 @@ -0,0 +1,85 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with CSRF protection using either session or cookie-based tokens. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081. It includes CSRF protection, which can be configured + to use either session-based tokens or cookie-based global secrets. The script also sets up a basic route for + serving an index page with a CSRF token and a POST route to handle form submissions. + +.PARAMETER Type + Specifies the type of CSRF protection to use. Valid values are 'Cookie' and 'Session'. Default is 'Session'. + +.EXAMPLE + To run the sample: ./Web-Csrf.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/ -Method Get + Invoke-RestMethod -Uri http://localhost:8081/token -Method Post + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-Csrf.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +param( + [Parameter()] + [ValidateSet('Cookie', 'Session')] + [string] + $Type = 'Session' +) + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + # set view engine to pode renderer + Set-PodeViewEngine -Type Pode + + # set csrf middleware, then either session middleware, or cookie global secret + switch ($Type.ToLowerInvariant()) { + 'cookie' { + Set-PodeCookieSecret -Value 'rem' -Global + Enable-PodeCsrfMiddleware -UseCookies + } + + 'session' { + Enable-PodeSessionMiddleware -Duration 120 + Enable-PodeCsrfMiddleware + } + } + + # GET request for index page, and to make a token + # this route will work, as GET methods are ignored by CSRF by default + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + $token = (New-PodeCsrfToken) + Write-PodeViewResponse -Path 'index-csrf' -Data @{ 'csrfToken' = $token } -FlashMessages + } + + # POST route for form with and without csrf token + Add-PodeRoute -Method Post -Path '/token' -ScriptBlock { + Move-PodeResponseUrl -Url '/' + } + +} \ No newline at end of file diff --git a/examples/Web-FuncsToRoutes.ps1 b/examples/Web-FuncsToRoutes.ps1 new file mode 100644 index 000000000..3e7e329bf --- /dev/null +++ b/examples/Web-FuncsToRoutes.ps1 @@ -0,0 +1,69 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with basic authentication and dynamic route generation. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081. It includes basic authentication, error logging, + and dynamic route generation for specified commands. Each route requires authentication. + +.EXAMPLE + To run the sample: ./Web-FuncsToRoutes.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/Get-ChildItem -Method Get -ContentType 'application/json' -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' } + Invoke-RestMethod -Uri http://localhost:8081/Get-Host -Method Get -ContentType 'application/json' -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' } + Invoke-RestMethod -Uri http://localhost:8081/Invoke-Expression -Method Post -ContentType 'application/json' -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' } + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-FuncsToRoutes.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # setup basic auth (base64> username:password in header) + New-PodeAuthScheme -Basic | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { + param($username, $password) + + # here you'd check a real user storage, this is just for example + if ($username -eq 'morty' -and $password -eq 'pickle') { + return @{ + User = @{ + ID ='M0R7Y302' + Name = 'Morty' + Type = 'Human' + } + } + } + + return @{ Message = 'Invalid details supplied' } + } + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + # make routes for functions - with every route requires authentication + ConvertTo-PodeRoute -Commands @('Get-ChildItem', 'Get-Host', 'Invoke-Expression') -Authentication Validate -Verbose + + # make routes for every exported command in Pester + # ConvertTo-PodeRoute -Module Pester -Verbose + +} diff --git a/examples/Web-Gui.ps1 b/examples/Web-Gui.ps1 new file mode 100644 index 000000000..26c8d1459 --- /dev/null +++ b/examples/Web-Gui.ps1 @@ -0,0 +1,55 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with desktop GUI capabilities. + +.DESCRIPTION + This script sets up a Pode server listening on ports 8081 and 8091. It includes a route to handle GET requests + and sets up the server to run as a desktop GUI application using the Pode view engine. + +.EXAMPLE + To run the sample: ./Web-Gui.ps1 + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-Gui.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http -Name 'local1' + Add-PodeEndpoint -Address localhost -Port 8091 -Protocol Http -Name 'local2' + + # tell this server to run as a desktop gui + Show-PodeGui -Title 'Pode Desktop Application' -Icon '../images/icon.png' -EndpointName 'local2' -ResizeMode 'NoResize' + + # set view engine to pode renderer + Set-PodeViewEngine -Type Pode + + # GET request for web page on "localhost:8081/" + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Write-PodeViewResponse -Path 'gui' -Data @{ 'numbers' = @(1, 2, 3); } + } + +} \ No newline at end of file diff --git a/examples/Web-GzipRequest.ps1 b/examples/Web-GzipRequest.ps1 new file mode 100644 index 000000000..257e588ca --- /dev/null +++ b/examples/Web-GzipRequest.ps1 @@ -0,0 +1,51 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with error logging and a route to handle gzip'd JSON. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081. It includes error logging and a route to handle POST requests that receive gzip'd JSON data. + +.EXAMPLE + To run the sample: ./Web-GzipRequest.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/users -Method Post + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-GzipRequest.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + # GET request that receives gzip'd json + Add-PodeRoute -Method Post -Path '/users' -ScriptBlock { + Write-PodeJsonResponse -Value $WebEvent.Data + } + +} \ No newline at end of file diff --git a/examples/Web-Hostname.ps1 b/examples/Web-Hostname.ps1 new file mode 100644 index 000000000..c13a4af78 --- /dev/null +++ b/examples/Web-Hostname.ps1 @@ -0,0 +1,80 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with multiple endpoints. + +.DESCRIPTION + This script sets up a Pode server listening on multiple endpoints. + It demonstrates how to handle GET requests, serve static assets, and download files using Pode's view engine. + +.PARAMETER Port + The port number on which the server will listen. Default is 8081. + +.EXAMPLE + To run the sample: ./Web-Hostname.ps1 + + Invoke-RestMethod -Uri http://pode3.foo.com:8081/ -Method Get + Invoke-RestMethod -Uri http://pode3.foo.com:8081/download -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-Hostname.ps1 + +.NOTES + Author: Pode Team + License: MIT License + Administrator privilege is required. + You will need to add "127.0.0.1 pode.foo.com" to your hosts file. +#> +param( + [int] + $Port = 8081 +) + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# Administrator privilege is required + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 at pode.foo.com +# -- You will need to add "127.0.0.1 pode.foo.com" to your hosts file +Start-PodeServer -Threads 2 { + + # listen on localhost:8081 + Add-PodeEndpoint -Address pode3.foo.com -Port $Port -Protocol Http + Add-PodeEndpoint -Address pode2.foo.com -Port $Port -Protocol Http + Add-PodeEndpoint -Address 127.0.0.1 -Hostname pode.foo.com -Port $Port -Protocol Http + Add-PodeEndpoint -Hostname pode4.foo.com -Port $Port -Protocol Http -LookupHostname + + # logging + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + # set view engine to pode renderer + Set-PodeViewEngine -Type Pode + + # STATIC asset folder route + Add-PodeStaticRoute -Path '/assets' -Source './assets' -Defaults @('index.html') + + # GET request for web page on "localhost:8081/" + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Write-PodeViewResponse -Path 'web-static' -Data @{ 'numbers' = @(1, 2, 3); } + } + + # GET request to download a file from static route + Add-PodeRoute -Method Get -Path '/download' -ScriptBlock { + Set-PodeResponseAttachment -Path '/assets/images/Fry.png' + } +} \ No newline at end of file diff --git a/examples/Web-HostnameKestrel.ps1 b/examples/Web-HostnameKestrel.ps1 new file mode 100644 index 000000000..477889380 --- /dev/null +++ b/examples/Web-HostnameKestrel.ps1 @@ -0,0 +1,84 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with Kestrel and various endpoints. + +.DESCRIPTION + This script sets up a Pode server listening on multiple endpoints using Kestrel. + It demonstrates how to handle GET requests, serve static assets, and download files using Pode's view engine. + +.PARAMETER Port + The port number on which the server will listen. Default is 8081. + +.EXAMPLE + To run the sample: ./Web-HostnameKestrel.ps1 + + Invoke-RestMethod -Uri http://pode3.foo.com:8081/ -Method Get + Invoke-RestMethod -Uri http://pode3.foo.com:8081/download -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-HostnameKestrel.ps1 + +.NOTES + Author: Pode Team + License: MIT License + Administrator privilege is required + You will need to add "127.0.0.1 pode.foo.com" to your hosts file. +#> +param( + [int] + $Port = 8081 +) + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# Administrator privilege is required + +# or just: +# Import-Module Pode + +# you will require the Pode.Kestrel module for this example +Import-Module Pode.Kestrel + +# create a server, and start listening on port 8081 at pode.foo.com +# -- You will need to add "127.0.0.1 pode.foo.com" to your hosts file +Start-PodeServer -Threads 2 -ListenerType Kestrel { + + # listen on localhost:8081 + Add-PodeEndpoint -Address pode3.foo.com -Port $Port -Protocol Http + Add-PodeEndpoint -Address pode2.foo.com -Port $Port -Protocol Http + Add-PodeEndpoint -Address localhost -Hostname pode.foo.com -Port $Port -Protocol Http + Add-PodeEndpoint -Hostname pode4.foo.com -Port $Port -Protocol Http -LookupHostname + + # logging + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + # set view engine to pode renderer + Set-PodeViewEngine -Type Pode + + # STATIC asset folder route + Add-PodeStaticRoute -Path '/assets' -Source './assets' -Defaults @('index.html') + + # GET request for web page on "localhost:8081/" + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Write-PodeViewResponse -Path 'web-static' -Data @{ 'numbers' = @(1, 2, 3); } + } + + # GET request to download a file from static route + Add-PodeRoute -Method Get -Path '/download' -ScriptBlock { + Set-PodeResponseAttachment -Path '/assets/images/Fry.png' + } + +} \ No newline at end of file diff --git a/examples/Web-Imports.ps1 b/examples/Web-Imports.ps1 new file mode 100644 index 000000000..7a89c76ed --- /dev/null +++ b/examples/Web-Imports.ps1 @@ -0,0 +1,65 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with various routes, access rules, logging, and request handling. + +.DESCRIPTION + This script sets up a Pode server listening on multiple endpoints with request redirection. + It demonstrates how to handle GET, POST, and other HTTP requests, set up access and limit rules, + implement custom logging, and serve web pages using Pode's view engine. + +.PARAMETER Port + The port number on which the server will listen. Default is 8081. + +.EXAMPLE + To run the sample: ./Web-Imports.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/ -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-Imports.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +param( + [int] + $Port = 8081 +) + +try { + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } + + # import modules + Import-Module -Name EPS -ErrorAction Stop +} +catch { throw } + +# or just: +# Import-Module Pode + + + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port $Port -Protocol Http + + # set view engine to pode renderer + Set-PodeViewEngine -Type Pode + + # GET request for web page on "localhost:8081/" + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Get-Module | Out-Default + Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(1, 2, 3); } + } + +} \ No newline at end of file diff --git a/examples/Web-Metrics.ps1 b/examples/Web-Metrics.ps1 new file mode 100644 index 000000000..ee17b9981 --- /dev/null +++ b/examples/Web-Metrics.ps1 @@ -0,0 +1,50 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with a route to check server uptime and restart count. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081. It includes a route to check the server's uptime + and the number of times the server has restarted. + +.EXAMPLE + To run the sample: ./Web-Metrics.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/uptime -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-Metrics.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +Start-PodeServer -Threads 2 { + + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + Add-PodeRoute -Method Get -Path '/uptime' -ScriptBlock { + Write-PodeJsonResponse -Value @{ + Restarts = (Get-PodeServerRestartCount) + Uptime = @{ + Session = (Get-PodeServerUptime) + Total = (Get-PodeServerUptime -Total) + } + } + } + +} \ No newline at end of file diff --git a/examples/web-pages.ps1 b/examples/Web-Pages.ps1 similarity index 50% rename from examples/web-pages.ps1 rename to examples/Web-Pages.ps1 index 2d1dcb53b..9cd174a25 100644 --- a/examples/web-pages.ps1 +++ b/examples/Web-Pages.ps1 @@ -1,31 +1,75 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with various routes, access rules, logging, and request handling. + +.DESCRIPTION + This script sets up a Pode server listening on multiple endpoints with request redirection. + It demonstrates how to handle GET, POST, and other HTTP requests, set up access and limit rules, + implement custom logging, and serve web pages using Pode's view engine. + +.PARAMETER Port + The port number on which the server will listen. Default is 8081. + +.EXAMPLE + To run the sample: ./Web-Pages.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/ -Method Get + Invoke-RestMethod -Uri http://localhost:8081/variable -Method Get + Invoke-RestMethod -Uri http://localhost:8081/error -Method Get + Invoke-RestMethod -Uri http://localhost:8081/redirect -Method Get + Invoke-RestMethod -Uri http://localhost:8081/redirect-port -Method Get + Invoke-RestMethod -Uri http://localhost:8081/download -Method Get + Invoke-RestMethod -Uri http://localhost:8081/testuser/details -Method Post + Invoke-RestMethod -Uri http://localhost:8081/all -Method Merge + Invoke-RestMethod -Uri http://localhost:8081//api/test/hello -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-Pages.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> param( [int] - $Port = 8085 + $Port = 8081 ) -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } # or just: # Import-Module Pode -# create a server, and start listening on port 8085 +# create a server, and start listening on port 8081 Start-PodeServer -Threads 2 -Verbose { - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8090 -Protocol Http -Name '8090Address' - Add-PodeEndpoint -Address * -Port $Port -Protocol Http -Name '8085Address' -RedirectTo '8090Address' + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8090 -Protocol Http -Name '8090Address' + Add-PodeEndpoint -Address localhost -Port $Port -Protocol Http -Name "$($Port)Address" -RedirectTo '8090Address' # allow the local ip and some other ips - Add-PodeAccessRule -Access Allow -Type IP -Values @('127.0.0.1', '[::1]') - Add-PodeAccessRule -Access Allow -Type IP -Values @('192.169.0.1', '192.168.0.2') + # Add-PodeAccessRule -Access Allow -Type IP -Values @('127.0.0.1', '[::1]') + # Add-PodeAccessRule -Access Allow -Type IP -Values @('192.169.0.1', '192.168.0.2') # deny an ip - Add-PodeAccessRule -Access Deny -Type IP -Values 10.10.10.10 - Add-PodeAccessRule -Access Deny -Type IP -Values '10.10.0.0/24' - Add-PodeAccessRule -Access Deny -Type IP -Values all + # Add-PodeAccessRule -Access Deny -Type IP -Values 10.10.10.10 + # Add-PodeAccessRule -Access Deny -Type IP -Values '10.10.0.0/24' + # Add-PodeAccessRule -Access Deny -Type IP -Values all # limit - Add-PodeLimitRule -Type IP -Values all -Limit 100 -Seconds 5 + # Add-PodeLimitRule -Type IP -Values all -Limit 100 -Seconds 5 # log requests to the terminal New-PodeLoggingMethod -Terminal -Batch 10 -BatchTimeout 10 | Enable-PodeRequestLogging @@ -49,7 +93,7 @@ Start-PodeServer -Threads 2 -Verbose { Use-PodeRoutes -Path './routes' - # GET request for web page on "localhost:8085/" + # GET request for web page on "localhost:8081/" Add-PodeRoute -Method Get -Path '/' -ScriptBlock { # $WebEvent.Request | Write-PodeLog -Name 'custom' Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(1, 2, 3); } @@ -100,7 +144,7 @@ Start-PodeServer -Threads 2 -Verbose { Write-PodeJsonResponse -Value @{ 'value' = 'works for every hello route' } } - $hmm = 'well well' - Add-PodeRoute -Method Get -Path '/script' -FilePath './modules/route_script.ps1' + $script:hmm = 'well well' + Add-PodeRoute -Method Get -Path '/script' -FilePath './modules/RouteScript.ps1' } \ No newline at end of file diff --git a/examples/web-pages-docker.ps1 b/examples/Web-PagesDocker.ps1 similarity index 57% rename from examples/web-pages-docker.ps1 rename to examples/Web-PagesDocker.ps1 index f01281e7b..8ee48bf34 100644 --- a/examples/web-pages-docker.ps1 +++ b/examples/Web-PagesDocker.ps1 @@ -1,14 +1,33 @@ -Import-Module /usr/local/share/powershell/Modules/Pode/Pode.psm1 -Force -ErrorAction Stop - <# -docker-compose up --force-recreate --build +.SYNOPSIS + A sample PowerShell script to set up a Pode server with various routes, tasks, and security. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081. It demonstrates how to handle GET and PUT requests, + set up security, define tasks, and use Pode's view engine. + +.EXAMPLE + To build and start the Docker container, use: + docker-compose up --force-recreate --build + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-Cookies.ps1 + +.NOTES + Author: Pode Team + License: MIT License #> +try { + Import-Module /usr/local/share/powershell/Modules/Pode/Pode.psm1 -Force -ErrorAction Stop +} +catch { throw } + -# create a server, and start listening on port 8085 +# create a server, and start listening on port 8081 Start-PodeServer -Threads 2 { - # listen on *:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http + # listen on *:8081 + Add-PodeEndpoint -Address * -Port 8081 -Protocol Http Set-PodeSecurity -Type Simple # set view engine to pode renderer @@ -20,7 +39,7 @@ Start-PodeServer -Threads 2 { return @{ InnerValue = 'hey look, a value!' } } - # GET request for web page on "localhost:8085/" + # GET request for web page on "localhost:8081/" Add-PodeRoute -Method Get -Path '/' -ScriptBlock { Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(1, 2, 3); } } diff --git a/examples/Web-PagesHttps.ps1 b/examples/Web-PagesHttps.ps1 new file mode 100644 index 000000000..8c6dabcc3 --- /dev/null +++ b/examples/Web-PagesHttps.ps1 @@ -0,0 +1,80 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with various HTTPS certificate options. + +.DESCRIPTION + This script sets up a Pode server listening on port 8443 with HTTPS enabled using different certificate options based on the input parameter. + It demonstrates how to handle GET requests and serve web pages with Pode's view engine. + +.PARAMETER CertType + Specifies the type of certificate to use for HTTPS. Valid values are 'SelfSigned', 'CertificateWithPassword', 'Certificate', and 'CertificateThumbprint'. + Default is 'SelfSigned'. + +.EXAMPLE + To run the sample: ./Web-PagesHttps.ps1 + + Invoke-RestMethod -Uri https://localhost:8443/ -Method Get + Invoke-RestMethod -Uri https://localhost:8443/error -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-PagesHttps.ps1 +.NOTES + Author: Pode Team + License: MIT License +#> +param( + [string] + [ValidateSet('SelfSigned', 'CertificateWithPassword', 'Certificate' , 'CertificateThumbprint')] + $CertType = 'SelfSigned' +) + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + + + +# or just: +# Import-Module Pode + +# create a server, flagged to generate a self-signed cert for dev/testing +Start-PodeServer { + + # bind to ip/port and set as https with self-signed cert + switch ($CertType) { + 'SelfSigned' { + Add-PodeEndpoint -Address localhost -Port 8443 -Protocol Https -SelfSigned + } + 'CertificateWithPassword' { + Add-PodeEndpoint -Address localhost -Port 8443 -Protocol Https -Certificate './certs/cert.pem' -CertificateKey './certs/key.pem' -CertificatePassword 'test' + } + 'Certificate' { + Add-PodeEndpoint -Address localhost -Port 8443 -Protocol Https -Certificate './certs/cert_nodes.pem' -CertificateKey './certs/key_nodes.pem' + } + 'CertificateThumbprint' { Add-PodeEndpoint -Address localhost -Port 8443 -Protocol Https -CertificateThumbprint '2A623A8DC46ED42A13B27DD045BFC91FDDAEB957' } + } + # set view engine for web pages + Set-PodeViewEngine -Type Pode + + # GET request for web page at "/" + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(1, 2, 3); } + } + + # GET request throws fake "500" server error status code + Add-PodeRoute -Method Get -Path '/error' -ScriptBlock { + Set-PodeResponseStatus -Code 500 + } + +} diff --git a/examples/Web-PagesKestrel.ps1 b/examples/Web-PagesKestrel.ps1 new file mode 100644 index 000000000..e3402f6c9 --- /dev/null +++ b/examples/Web-PagesKestrel.ps1 @@ -0,0 +1,139 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with Kestrel and various routes, access rules, and custom logging. + +.DESCRIPTION + This script sets up a Pode server listening on multiple endpoints with request redirection using Kestrel. + It demonstrates how to handle GET requests, set up access rules, implement custom logging, and handle various routes including redirects and file downloads. + +.PARAMETER Port + The port number on which the server will listen. Default is 8081. + +.EXAMPLE + To run the sample: ./Web-PagesKestrel.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/ -Method Get + Invoke-RestMethod -Uri http://localhost:8081/variable -Method Get + Invoke-RestMethod -Uri http://localhost:8081/error -Method Get + Invoke-RestMethod -Uri http://localhost:8081/redirect -Method Get + Invoke-RestMethod -Uri http://localhost:8081/redirect-port -Method Get + Invoke-RestMethod -Uri http://localhost:8081/download -Method Get + Invoke-RestMethod -Uri http://localhost:8081/testuser/details -Method Post + Invoke-RestMethod -Uri http://localhost:8081/all -Method Merge + Invoke-RestMethod -Uri http://localhost:8081//api/test/hello -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-PagesKestrel.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +param( + [int] + $Port = 8081 +) + +try { + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } + + # you will require the Pode.Kestrel module for this example + Import-Module Pode.Kestrel -Force -ErrorAction Stop +} +catch { throw } + +# or just: +# Import-Module Pode + + +# create a server, and start listening on port 8081 using kestrel +Start-PodeServer -Threads 2 -ListenerType Kestrel { + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8090 -Protocol Http -Name '8090Address' + Add-PodeEndpoint -Address localhost -Port $Port -Protocol Http -Name '8081Address' -RedirectTo '8090Address' + + # allow the local ip and some other ips + # Add-PodeAccessRule -Access Allow -Type IP -Values @('127.0.0.1', '[::1]') + # Add-PodeAccessRule -Access Allow -Type IP -Values @('192.169.0.1', '192.168.0.2') + + # deny an ip + # Add-PodeAccessRule -Access Deny -Type IP -Values 10.10.10.10 + # Add-PodeAccessRule -Access Deny -Type IP -Values '10.10.0.0/24' + # Add-PodeAccessRule -Access Deny -Type IP -Values all + + # log requests to the terminal + New-PodeLoggingMethod -Terminal -Batch 10 -BatchTimeout 10 | Enable-PodeRequestLogging + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + # set view engine to pode renderer + Set-PodeViewEngine -Type Pode + + # wire up a custom logger + $logType = New-PodeLoggingMethod -Custom -ScriptBlock { + param($item) + $item.HttpMethod | Out-Default + } + + $logType | Add-PodeLogger -Name 'custom' -ScriptBlock { + param($item) + return @{ + HttpMethod = $item.HttpMethod + } + } + + # GET request for web page on "localhost:8081/" + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + # $WebEvent.Request | Write-PodeLog -Name 'custom' + Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(1, 2, 3); } + } + + # GET request throws fake "500" server error status code + Add-PodeRoute -Method Get -Path '/error' -ScriptBlock { + Set-PodeResponseStatus -Code 500 + } + + # GET request to page that merely redirects to google + Add-PodeRoute -Method Get -Path '/redirect' -ScriptBlock { + Move-PodeResponseUrl -Url 'https://google.com' + } + + # GET request that redirects to same host, just different port + Add-PodeRoute -Method Get -Path '/redirect-port' -ScriptBlock { + if ($WebEvent.Request.Url.Port -ne 8086) { + Move-PodeResponseUrl -Port 8086 + } + else { + Write-PodeJsonResponse -Value @{ 'value' = 'you got redirected!'; } + } + } + + # GET request to download a file + Add-PodeRoute -Method Get -Path '/download' -ScriptBlock { + Set-PodeResponseAttachment -Path 'Anger.jpg' + } + + # GET request with parameters + Add-PodeRoute -Method Get -Path '/:userId/details' -ScriptBlock { + Write-PodeJsonResponse -Value @{ 'userId' = $WebEvent.Parameters['userId'] } + } + + # ALL request, that supports every method and it a default drop route + Add-PodeRoute -Method * -Path '/all' -ScriptBlock { + Write-PodeJsonResponse -Value @{ 'value' = 'works for every http method' } + } + + Add-PodeRoute -Method Get -Path '/api/*/hello' -ScriptBlock { + Write-PodeJsonResponse -Value @{ 'value' = 'works for every hello route' } + } + + $script:hmm = 'well well' + Add-PodeRoute -Method Get -Path '/script' -FilePath './modules/RouteScript.ps1' + +} \ No newline at end of file diff --git a/examples/Web-PagesMD.ps1 b/examples/Web-PagesMD.ps1 new file mode 100644 index 000000000..ced109f82 --- /dev/null +++ b/examples/Web-PagesMD.ps1 @@ -0,0 +1,93 @@ +<# +.SYNOPSIS + Sample script to set up a Pode server that listens on localhost:8081 and responds to GET requests. + +.DESCRIPTION + This script initializes a Pode server and sets it to listen on localhost:8081. It uses the Markdown + view engine to render responses. The server will respond to GET requests at the root path ('/') by + serving an 'index' view. + +.PARAMETER ScriptPath + Path of the script being executed. + +.PARAMETER podePath + Path of the Pode module. + +.EXAMPLE + To run the sample: ./Web-PagesMD.ps1 + + Run this script to start the Pode server and navigate to 'http://localhost:8081' in your browser. + The server will respond to GET requests at the root path with the 'index' view. + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-PagesMD.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + # set view engine + Set-PodeViewEngine -Type Markdown + + # GET request for web page on "localhost:8081/" + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Write-PodeViewResponse -Path 'index' + } + +}try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + # set view engine + Set-PodeViewEngine -Type Markdown + + # GET request for web page on "localhost:8081/" + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Write-PodeViewResponse -Path 'index' + } + +} \ No newline at end of file diff --git a/examples/Web-PagesSimple.ps1 b/examples/Web-PagesSimple.ps1 new file mode 100644 index 000000000..55e0aaf5c --- /dev/null +++ b/examples/Web-PagesSimple.ps1 @@ -0,0 +1,66 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with multiple endpoints and request handling. + +.DESCRIPTION + This script sets up a Pode server listening on multiple endpoints with request redirection. + It demonstrates how to handle GET requests and redirect requests from one endpoint to another. + The script includes examples of using a parameter for the port number and setting up error logging. + +.PARAMETER Port + The port number on which the server will listen. Default is 8081. + +.EXAMPLE + To run the sample: ./Web-PagesSimple.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/ -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-PagesSimple.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +param( + [int] + $Port = 8081 +) + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8090 -Protocol Http -Name '8090Address' + Add-PodeEndpoint -Address localhost -Port $Port -Protocol Http -Name '8081Address' -RedirectTo '8090Address' + + # log errors to the terminal + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + # set view engine to pode renderer + Set-PodeViewEngine -Type Pode + + # GET request for web page on "localhost:8081/" + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(1, 2, 3); } + } + +} \ No newline at end of file diff --git a/examples/web-pages-using.ps1 b/examples/Web-PagesUsing.ps1 similarity index 52% rename from examples/web-pages-using.ps1 rename to examples/Web-PagesUsing.ps1 index f0b5467e2..061713a2d 100644 --- a/examples/web-pages-using.ps1 +++ b/examples/Web-PagesUsing.ps1 @@ -1,5 +1,57 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with various routes, middleware, and custom functions. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081. It demonstrates how to handle GET requests, + use middleware, export and use custom functions, and set up timers. The script includes examples of + using `$using:` scope for variables in script blocks and middleware. + +.EXAMPLE + To run the sample: ./Web-PagesUsing.ps1 + + # Root route + Invoke-RestMethod -Uri http://localhost:8081/ -Method Get + + # Random route + Invoke-RestMethod -Uri http://localhost:8081/random -Method Get + + # Inner function route + Invoke-RestMethod -Uri http://localhost:8081/inner-func -Method Get + + # Outer function route + Invoke-RestMethod -Uri http://localhost:8081/outer-func -Method Get + + # Greetings route + Invoke-RestMethod -Uri http://localhost:8081/greetings -Method Get + + # Sub-greetings route + Invoke-RestMethod -Uri http://localhost:8081/sub-greetings -Method Get + + # Timer route + Invoke-RestMethod -Uri http://localhost:8081/timer -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-PagesUsing.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } # or just: # Import-Module Pode @@ -11,10 +63,10 @@ function Write-MyOuterResponse { Write-PodeJsonResponse -Value @{ Message = 'From an outer function' } } -# create a server, and start listening on port 8085 +# create a server, and start listening on port 8081 Start-PodeServer -Threads 2 { - # listen on localhost:8090 - Add-PodeEndpoint -Address * -Port 8090 -Protocol Http + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http # log requests to the terminal New-PodeLoggingMethod -Terminal -Batch 10 -BatchTimeout 10 | Enable-PodeRequestLogging @@ -24,7 +76,7 @@ Start-PodeServer -Threads 2 { Set-PodeViewEngine -Type Pode # load file funcs - Use-PodeScript -Path ./modules/imported-funcs.ps1 + Use-PodeScript -Path ./modules/Imported-Funcs.ps1 $innerfoo = 'inner-bar' $inner_ken = 'General Kenobi' @@ -45,7 +97,7 @@ Start-PodeServer -Threads 2 { return $true } - # GET request for web page on "localhost:8090/" + # GET request for web page on "localhost:8081/" Add-PodeRoute -Method Get -Path '/' -ScriptBlock { $using:innerfoo | Out-Default $using:outerfoo | Out-Default diff --git a/examples/Web-RestApi.ps1 b/examples/Web-RestApi.ps1 new file mode 100644 index 000000000..bcc7f1311 --- /dev/null +++ b/examples/Web-RestApi.ps1 @@ -0,0 +1,70 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with HTTP endpoints and request logging. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081 with various HTTP endpoints for GET and POST requests. + It includes request logging with batching and dual mode for IPv4/IPv6. + +.EXAMPLE + To run the sample: ./Rest-Api.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/api/test -Method Get + Invoke-RestMethod -Uri http://localhost:8081/api/test -Method Post + Invoke-RestMethod -Uri http://localhost:8081/api/users/usertest -Method Get + Invoke-RestMethod -Uri http://localhost:8081/api/users/usertest/message -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-RestApi.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8086 +Start-PodeServer { + + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http -DualMode + + # request logging + New-PodeLoggingMethod -Terminal -Batch 10 -BatchTimeout 10 | Enable-PodeRequestLogging + + # can be hit by sending a GET request to "localhost:8086/api/test" + Add-PodeRoute -Method Get -Path '/api/test' -ScriptBlock { + Write-PodeJsonResponse -Value @{ 'hello' = 'world'; } + } + + # can be hit by sending a POST request to "localhost:8086/api/test" + Add-PodeRoute -Method Post -Path '/api/test' -ContentType 'application/json' -ScriptBlock { + Write-PodeJsonResponse -Value @{ 'hello' = 'world'; 'name' = $WebEvent.Data['name']; } + } + + # returns details for an example user + Add-PodeRoute -Method Get -Path '/api/users/:userId' -ScriptBlock { + Write-PodeJsonResponse -Value @{ 'user' = $WebEvent.Parameters['userId']; } + } + + # returns details for an example user + Add-PodeRoute -Method Get -Path '/api/users/:userId/messages' -ScriptBlock { + Write-PodeJsonResponse -Value @{ 'user' = $WebEvent.Parameters['userId']; } + } + +} \ No newline at end of file diff --git a/examples/web-rest-openapi.ps1 b/examples/Web-RestOpenApi.ps1 similarity index 64% rename from examples/web-rest-openapi.ps1 rename to examples/Web-RestOpenApi.ps1 index 0f6e25598..ab7be05aa 100644 --- a/examples/web-rest-openapi.ps1 +++ b/examples/Web-RestOpenApi.ps1 @@ -1,21 +1,61 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with OpenAPI integration and basic authentication. + +.DESCRIPTION + This script sets up a Pode server listening on multiple endpoints with OpenAPI documentation. + It demonstrates how to handle GET, POST, and PUT requests, use OpenAPI for documenting APIs, and implement basic authentication. + The script includes routes under the '/api' path and provides various OpenAPI viewers such as Swagger, ReDoc, RapiDoc, StopLight, Explorer, and RapiPdf. + +.EXAMPLE + To run the sample: ./Web-RestOpenApi.ps1 + + OpenAPI Info: + Specification: + http://localhost:8081/openapi + http://localhost:8082/openapi + Documentation: + http://localhost:8081/docs + http://localhost:8082/docs + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-RestOpenApi.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } Start-PodeServer { - Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http -Name 'user' - Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http -Name 'admin' + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http -Name 'user' + Add-PodeEndpoint -Address localhost -Port 8082 -Protocol Http -Name 'admin' New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - Enable-PodeOpenApi -Title 'OpenAPI Example' -RouteFilter '/api/*' -RestrictRoutes - Enable-PodeOpenApiViewer -Type Swagger -Path '/docs/swagger' - Enable-PodeOpenApiViewer -Type ReDoc -Path '/docs/redoc' - Enable-PodeOpenApiViewer -Type RapiDoc -Path '/docs/rapidoc' - Enable-PodeOpenApiViewer -Type StopLight -Path '/docs/stoplight' - Enable-PodeOpenApiViewer -Type Explorer -Path '/docs/explorer' - Enable-PodeOpenApiViewer -Type RapiPdf -Path '/docs/rapipdf' - Enable-PodeOpenApiViewer -Editor -Path '/docs/editor' - Enable-PodeOpenApiViewer -Bookmarks -Path '/docs' + Enable-PodeOpenApi -DisableMinimalDefinitions + Add-PodeOAInfo -Title 'OpenAPI Example' + Enable-PodeOAViewer -Type Swagger -Path '/docs/swagger' + Enable-PodeOAViewer -Type ReDoc -Path '/docs/redoc' + Enable-PodeOAViewer -Type RapiDoc -Path '/docs/rapidoc' + Enable-PodeOAViewer -Type StopLight -Path '/docs/stoplight' + Enable-PodeOAViewer -Type Explorer -Path '/docs/explorer' + Enable-PodeOAViewer -Type RapiPdf -Path '/docs/rapipdf' + Enable-PodeOAViewer -Editor -Path '/docs/editor' + Enable-PodeOAViewer -Bookmarks -Path '/docs' New-PodeAuthScheme -Basic | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { return @{ diff --git a/examples/web-rest-openapi-shared.ps1 b/examples/Web-RestOpenApiShared.ps1 similarity index 63% rename from examples/web-rest-openapi-shared.ps1 rename to examples/Web-RestOpenApiShared.ps1 index 3571801c0..cda0344e3 100644 --- a/examples/web-rest-openapi-shared.ps1 +++ b/examples/Web-RestOpenApiShared.ps1 @@ -1,6 +1,44 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop -#Import-Module -Name powershell-yaml -Force -ErrorAction Stop +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with OpenAPI integration and basic authentication. + +.DESCRIPTION + This script sets up a Pode server listening on multiple endpoints with OpenAPI documentation. + It demonstrates how to handle GET and POST requests, use OpenAPI for documenting APIs, and implement basic authentication. + The script includes routes under the '/api' path and provides various OpenAPI viewers such as Swagger, ReDoc, RapiDoc, StopLight, Explorer, and RapiPdf. + +.EXAMPLE + To run the sample: ./Web-RestOpenApiShared.ps1 + + OpenAPI Info: + Specification: + . http://localhost:8080/openapi + . http://localhost:8081/openapi + Documentation: + . http://localhost:8080/bookmarks + . http://localhost:8081/bookmarks + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-RestOpenApiShared.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } Start-PodeServer { Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http -Name 'user' @@ -8,17 +46,20 @@ Start-PodeServer { New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - Enable-PodeOpenApi -Title 'OpenAPI Example' -RouteFilter '/api/*' -RestrictRoutes - Enable-PodeOpenApiViewer -Type Swagger - Enable-PodeOpenApiViewer -Type ReDoc - Enable-PodeOpenApiViewer -Type RapiDoc - Enable-PodeOpenApiViewer -Type StopLight - Enable-PodeOpenApiViewer -Type Explorer - Enable-PodeOpenApiViewer -Type RapiPdf + Enable-PodeOpenApi -DisableMinimalDefinitions + + Add-PodeOAInfo -Title 'OpenAPI Example' + + Enable-PodeOAViewer -Type Swagger + Enable-PodeOAViewer -Type ReDoc + Enable-PodeOAViewer -Type RapiDoc + Enable-PodeOAViewer -Type StopLight + Enable-PodeOAViewer -Type Explorer + Enable-PodeOAViewer -Type RapiPdf - Enable-PodeOpenApiViewer -Editor - Enable-PodeOpenApiViewer -Bookmarks + Enable-PodeOAViewer -Editor + Enable-PodeOAViewer -Bookmarks New-PodeAuthScheme -Basic | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { diff --git a/examples/Web-RestOpenApiSimple.ps1 b/examples/Web-RestOpenApiSimple.ps1 new file mode 100644 index 000000000..0f9ec193a --- /dev/null +++ b/examples/Web-RestOpenApiSimple.ps1 @@ -0,0 +1,92 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with OpenAPI integration. + +.DESCRIPTION + This script sets up a Pode server listening on multiple endpoints with OpenAPI documentation. + It demonstrates how to handle GET and POST requests, and how to use OpenAPI for documenting APIs. + The script includes routes under the '/api' path and provides Swagger and ReDoc viewers. + +.EXAMPLE + To run the sample: ./Web-RestOpenApiSimple.ps1 + + OpenAPI Info: + Specification: + . http://localhost:8080/openapi + . http://localhost:8081/openapi + Documentation: + . http://localhost:8080/bookmarks + . http://localhost:8081/bookmarks + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-RestOpenApiSimple.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +Start-PodeServer { + Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http -Name 'user' + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http -Name 'admin' + + Enable-PodeOpenApi -DisableMinimalDefinitions + + Add-PodeOAInfo -Title 'OpenAPI Example' + + Enable-PodeOAViewer -Type Swagger -DarkMode + Enable-PodeOAViewer -Type ReDoc + Enable-PodeOAViewer -Bookmarks -Path '/docs' + + Add-PodeRoute -Method Get -Path '/api/resources' -EndpointName 'user' -ScriptBlock { + Set-PodeResponseStatus -Code 200 + } + + + Add-PodeRoute -Method Post -Path '/api/resources' -ScriptBlock { + Set-PodeResponseStatus -Code 200 + } + + + Add-PodeRoute -Method Get -Path '/api/users/:userId' -ScriptBlock { + Write-PodeJsonResponse -Value @{ Name = 'Rick'; UserId = $WebEvent.Parameters['userId'] } + } -PassThru | Set-PodeOARouteInfo -PassThru | + Set-PodeOARequest -Parameters @( + (New-PodeOAIntProperty -Name 'userId' -Enum @(100, 300, 999) -Required | ConvertTo-PodeOAParameter -In Path) + ) + + + Add-PodeRoute -Method Get -Path '/api/users' -ScriptBlock { + Write-PodeJsonResponse -Value @{ Name = 'Rick'; UserId = $WebEvent.Query['userId'] } + } -PassThru | Set-PodeOARouteInfo -PassThru | + Set-PodeOARequest -Parameters @( + (New-PodeOAIntProperty -Name 'userId' -Required | ConvertTo-PodeOAParameter -In Query) + ) + + + Add-PodeRoute -Method Post -Path '/api/users' -ScriptBlock { + Write-PodeJsonResponse -Value @{ Name = $WebEvent.Data.Name; UserId = $WebEvent.Data.UserId } + } -PassThru | Set-PodeOARouteInfo -PassThru | + Set-PodeOARequest -RequestBody ( + New-PodeOARequestBody -Required -ContentSchemas @{ + 'application/json' = (New-PodeOAObjectProperty -Properties @( + (New-PodeOAStringProperty -Name 'Name' -MaxLength 5 -Pattern '[a-zA-Z]+'), + (New-PodeOAIntProperty -Name 'UserId') + )) + } + ) +} diff --git a/examples/Web-RouteEndpoints.ps1 b/examples/Web-RouteEndpoints.ps1 new file mode 100644 index 000000000..2e043dceb --- /dev/null +++ b/examples/Web-RouteEndpoints.ps1 @@ -0,0 +1,76 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with multiple endpoints and request handling. + +.DESCRIPTION + This script sets up a Pode server listening on multiple local IP addresses on port 8081. + It demonstrates how to handle GET requests for a web page, download a file, handle requests with parameters, + and redirect requests from one endpoint to another. + +.EXAMPLE + To run the sample: ./Web-RouteEndpoints.ps1 + + Invoke-RestMethod -Uri http://127.0.0.1:8081/ -Method Get + Invoke-RestMethod -Uri http://127.0.0.1:8081/download -Method Get + Invoke-RestMethod -Uri http://127.0.0.1:8081/testuser/details -Method Get + Invoke-RestMethod -Uri http://127.0.0.2:8082/something -Method Patch + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-RouteEndpoints.ps1 + +.NOTES + Author: Pode Team + License: MIT License + Administrator privilege required +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +#Administrator privilege required + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer { + + # listen on localhost:8081 + Add-PodeEndpoint -Address 127.0.0.1 -Port 8081 -Protocol Http -Name Endpoint1 + Add-PodeEndpoint -Address 127.0.0.2 -Port 8081 -Protocol Http -Name Endpoint2 + + # set view engine to pode + Set-PodeViewEngine -Type Pode + + # GET request for web page + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(1, 2, 3); } + } + + # GET request to download a file + Add-PodeRoute -Method Get -Path '/download' -ScriptBlock { + Set-PodeResponseAttachment -Path 'Anger.jpg' + } + + # GET request with parameters + Add-PodeRoute -Method Get -Path '/:userId/details' -ScriptBlock { + Write-PodeJsonResponse -Value @{ 'userId' = $WebEvent.Parameters['userId'] } + } + + # ALL requests for 127.0.0.2 to 127.0.0.1 + Add-PodeRoute -Method * -Path * -EndpointName Endpoint2 -ScriptBlock { + Move-PodeResponseUrl -Address 127.0.0.1 + } + +} \ No newline at end of file diff --git a/examples/Web-RouteGroup.ps1 b/examples/Web-RouteGroup.ps1 new file mode 100644 index 000000000..15c9f9ce0 --- /dev/null +++ b/examples/Web-RouteGroup.ps1 @@ -0,0 +1,106 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with middleware and basic authentication. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081. It demonstrates how to use middleware, + route groups, and basic authentication. The server includes routes under the '/api' and '/auth' paths, + with specific middleware and authentication requirements. + +.EXAMPLE + To run the sample: ./Web-RouteGroup.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/api/route1 -Method Get + Invoke-RestMethod -Uri http://localhost:8081/api/inner/route2 -Method Get + Invoke-RestMethod -Uri http://localhost:8081/api/inner/route3 -Method Get + Invoke-RestMethod -Uri http://localhost:8081/auth/route1 -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' } + Invoke-RestMethod -Uri http://localhost:8081/auth/route2 -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' } + Invoke-RestMethod -Uri http://localhost:8081/auth/route3 -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' } + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-RouteGroup.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + + +$message = 'Kenobi' + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + $mid1 = New-PodeMiddleware -ScriptBlock { + 'here1' | Out-Default + } + + $mid2 = New-PodeMiddleware -ScriptBlock { + 'here2' | Out-Default + } + + Add-PodeRouteGroup -Path '/api' -Middleware $mid1 -Routes { + Add-PodeRoute -Method Get -Path '/route1' -ScriptBlock { + Write-PodeJsonResponse -Value @{ ID = 1 } + } + + Add-PodeRouteGroup -Path '/inner' -Routes { + Add-PodeRoute -Method Get -Path '/route2' -Middleware $using:mid2 -ScriptBlock { + Write-PodeJsonResponse -Value @{ ID = 2 } + } + + Add-PodeRoute -Method Get -Path '/route3' -ScriptBlock { + "Hello there, $($using:message)" | Out-Default + Write-PodeJsonResponse -Value @{ ID = 3 } + } + } + } + + + # Invoke-RestMethod -Uri http://localhost:8081/auth/route1 -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' } + New-PodeAuthScheme -Basic | Add-PodeAuth -Name 'Basic' -Sessionless -ScriptBlock { + param($username, $password) + + # here you'd check a real user storage, this is just for example + if ($username -eq 'morty' -and $password -eq 'pickle') { + return @{ + User = @{ ID = 'M0R7Y302' } + } + } + + return @{ Message = 'Invalid details supplied' } + } + + Add-PodeRouteGroup -Path '/auth' -Authentication Basic -Routes { + Add-PodeRoute -Method Post -Path '/route1' -ScriptBlock { + Write-PodeJsonResponse -Value @{ ID = 1 } + } + + Add-PodeRoute -Method Post -Path '/route2' -ScriptBlock { + Write-PodeJsonResponse -Value @{ ID = 2 } + } + + Add-PodeRoute -Method Post -Path '/route3' -ScriptBlock { + Write-PodeJsonResponse -Value @{ ID = 3 } + } + } + +} \ No newline at end of file diff --git a/examples/Web-RouteListenNames.ps1 b/examples/Web-RouteListenNames.ps1 new file mode 100644 index 000000000..e0ba9ae4b --- /dev/null +++ b/examples/Web-RouteListenNames.ps1 @@ -0,0 +1,83 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with multiple endpoints and request handling. + +.DESCRIPTION + This script sets up a Pode server listening on multiple local IP addresses on port 8081. + It demonstrates how to handle GET requests for a web page, including specific handling for different endpoints, + downloading a file, and handling requests with parameters. + +.EXAMPLE + To run the sample: ./Web-RouteListenNames.ps1 + + Invoke-RestMethod -Uri http://127.0.0.1:8081/ -Method Get + Invoke-RestMethod -Uri http://127.0.0.2:8081/ -Method Get + Invoke-RestMethod -Uri http://127.0.0.3:8081/ -Method Get + Invoke-RestMethod -Uri http://127.0.0.4:8081/ -Method Get + Invoke-RestMethod -Uri http://127.0.0.2:8081/download -Method Get + Invoke-RestMethod -Uri http://127.0.0.3:8081/testuser/details -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-RouteListenNames.ps1 + +.NOTES + Author: Pode Team + License: MIT License + Administrator privilege required +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer { + + # listen on localhost:8081 + Add-PodeEndpoint -Address 127.0.0.1 -Port 8081 -Protocol Http -Name 'local1' + Add-PodeEndpoint -Address 127.0.0.2 -Port 8081 -Protocol Http -Name 'local2' + Add-PodeEndpoint -Address 127.0.0.3 -Port 8081 -Protocol Http -Name 'local3' + Add-PodeEndpoint -Address 127.0.0.4 -Port 8081 -Protocol Http -Name 'local4' + + # set view engine to pode + Set-PodeViewEngine -Type Pode + + # GET request for web page - all endpoints + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(1, 2, 3); } + } + + # GET request for web page - local2 endpoint + Add-PodeRoute -Method Get -Path '/' -EndpointName 'local2' -ScriptBlock { + Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(1, 2, 3, 4, 5, 6, 7, 8); } + } + + # GET request for web page - local3 and local4 endpoints + Add-PodeRoute -Method Get -Path '/' -EndpointName 'local3', 'local4' -ScriptBlock { + Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(2, 4, 6, 8, 10, 12, 14, 16); } + } + + # GET request to download a file + Add-PodeRoute -Method Get -Path '/download' -ScriptBlock { + Set-PodeResponseAttachment -Path 'Anger.jpg' + } + + # GET request with parameters + Add-PodeRoute -Method Get -Path '/:userId/details' -ScriptBlock { + Write-PodeJsonResponse -Value @{ 'userId' = $WebEvent.Parameters['userId'] } + } + +} \ No newline at end of file diff --git a/examples/Web-RouteProtocols.ps1 b/examples/Web-RouteProtocols.ps1 new file mode 100644 index 000000000..4052112c6 --- /dev/null +++ b/examples/Web-RouteProtocols.ps1 @@ -0,0 +1,74 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with multiple endpoints and request handling. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081 (HTTP) and 8082 (HTTPS). + It demonstrates how to handle GET requests for a web page, download a file, and handle requests with parameters. + Additionally, it shows how to redirect all HTTP requests to HTTPS. + +.EXAMPLE + To run the sample: ./Web-RouteProtocols.ps1 + + Invoke-RestMethod -Uri http://localhost:8082/ -Method Get + Invoke-RestMethod -Uri http://localhost:8081/download -Method Get + Invoke-RestMethod -Uri http://localhost:8081/testuser/details -Method Get + Invoke-RestMethod -Uri http://localhost:8082/download -Method Get + Invoke-RestMethod -Uri http://localhost:8082/testuser/details -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-RouteProtocols.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 and 8082 +Start-PodeServer { + + # listen on localhost:8080/8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http -Name Endpoint1 + Add-PodeEndpoint -Address localhost -Port 8082 -Protocol Https -Name Endpoint2 -SelfSigned + + # set view engine to pode + Set-PodeViewEngine -Type Pode + + # GET request for web page + Add-PodeRoute -Method Get -Path '/' -EndpointName Endpoint2 -ScriptBlock { + Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(1, 2, 3); } + } + + # GET request to download a file + Add-PodeRoute -Method Get -Path '/download' -ScriptBlock { + Set-PodeResponseAttachment -Path 'Anger.jpg' + } + + # GET request with parameters + Add-PodeRoute -Method Get -Path '/:userId/details' -ScriptBlock { + Write-PodeJsonResponse -Value @{ 'userId' = $WebEvent.Parameters['userId'] } + } + + # ALL requests for http only to redirect to https + Add-PodeRoute -Method * -Path * -EndpointName Endpoint1 -ScriptBlock { + Move-PodeResponseUrl -Protocol Https -Port 8081 + } + +} \ No newline at end of file diff --git a/examples/web-secrets.ps1 b/examples/Web-Secrets.ps1 similarity index 59% rename from examples/web-secrets.ps1 rename to examples/Web-Secrets.ps1 index 13c16d680..e2a27d2dd 100644 --- a/examples/web-secrets.ps1 +++ b/examples/Web-Secrets.ps1 @@ -1,18 +1,65 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with secret management using Azure Key Vault and a custom vault CLI. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081 with secret management capabilities. + It demonstrates how to register secret vaults with Azure Key Vault and a custom CLI, set and get secrets, + and manage secrets using Pode routes. Make sure to install the Microsoft.PowerShell.SecretManagement and Microsoft.PowerShell.SecretStore modules. + Also, you need to run "Connect-AzAccount" first for Azure Key Vault. + +.PARAMETER AzureSubscriptionId + The Azure Subscription ID for accessing the Azure Key Vault. + +.EXAMPLE + To run the sample: ./Web-Secrets.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/custom/ -Method Get + Invoke-RestMethod -Uri http://localhost:8081/custom/ -Method Post -body (@{value = 'my value'}| ConvertTo-Json) + Invoke-RestMethod -Uri http://localhost:8081/custom/somekey -Method Get + + Invoke-RestMethod -Uri http://localhost:8081/module/ -Method Get + Invoke-RestMethod -Uri http://localhost:8081/module/ -Method Post -body (@{value = 'my value'}| ConvertTo-Json) + + Invoke-RestMethod -Uri http://localhost:8081/adhoc/mykey -Method Post -body (@{value = 'my value'}| ConvertTo-Json) + Invoke-RestMethod -Uri http://localhost:8081/adhoc/mykey -Method Get + Invoke-RestMethod -Uri http://localhost:8081/adhoc/mykey -Method Delete + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-Secrets.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + param( [Parameter(Mandatory = $true)] [string] $AzureSubscriptionId ) -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } # or just: # Import-Module Pode Start-PodeServer -Threads 2 { # listen - Add-PodeEndpoint -Address * -Port 8080 -Protocol Http + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http # logging New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging diff --git a/examples/Web-SecretsLocal.ps1 b/examples/Web-SecretsLocal.ps1 new file mode 100644 index 000000000..f1a639d70 --- /dev/null +++ b/examples/Web-SecretsLocal.ps1 @@ -0,0 +1,90 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with secret management using Microsoft.PowerShell.SecretStore. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081 with secret management capabilities. + It demonstrates how to register a secret vault, set and get secrets, and manage secrets using Pode routes. + Make sure to install the Microsoft.PowerShell.SecretManagement and Microsoft.PowerShell.SecretStore modules. + +.EXAMPLE + To run the sample: ./Web-SecretsLocal.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/module/ -Method Get + Invoke-RestMethod -Uri http://localhost:8081/module/ -Method Post -body (@{value = 'my value'}| ConvertTo-Json) + + Invoke-RestMethod -Uri http://localhost:8081/adhoc/mykey -Method Post -body (@{value = 'my value'}| ConvertTo-Json) + Invoke-RestMethod -Uri http://localhost:8081/adhoc/mykey -Method Get + Invoke-RestMethod -Uri http://localhost:8081/adhoc/mykey -Method Delete + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-SecretsLocal.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +Start-PodeServer -Threads 2 { + # listen + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + # logging + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + + # secret manage local vault + $params = @{ + Name = 'PodeTest_LocalVault' + ModuleName = 'Microsoft.PowerShell.SecretStore' + UnlockSecret = 'Sup3rSecur3Pa$$word!' + } + + Register-PodeSecretVault @params + + + # set a secret in the local vault + Set-PodeSecret -Key 'hello' -Vault 'PodeTest_LocalVault' -InputObject 'world' + + + # mount a secret from local vault + Mount-PodeSecret -Name 'hello' -Vault 'PodeTest_LocalVault' -Key 'hello' + + + # routes to get/update secret in local vault + Add-PodeRoute -Method Get -Path '/module' -ScriptBlock { + Write-PodeJsonResponse @{ Value = $secret:hello } + } + + Add-PodeRoute -Method Post -Path '/module' -ScriptBlock { + $secret:hello = $WebEvent.Data.Value + } + + + Add-PodeRoute -Method Post -Path '/adhoc/:key' -ScriptBlock { + Set-PodeSecret -Key $WebEvent.Parameters['key'] -Vault 'PodeTest_LocalVault' -InputObject $WebEvent.Data['value'] + Mount-PodeSecret -Name $WebEvent.Data['name'] -Vault 'PodeTest_LocalVault' -Key $WebEvent.Parameters['key'] + } + + Add-PodeRoute -Method Delete -Path '/adhoc/:key' -ScriptBlock { + Remove-PodeSecret -Key $WebEvent.Parameters['key'] -Vault 'PodeTest_LocalVault' + Dismount-PodeSecret -Name $WebEvent.Parameters['key'] + } +} diff --git a/examples/Web-Sessions.ps1 b/examples/Web-Sessions.ps1 new file mode 100644 index 000000000..b07becc61 --- /dev/null +++ b/examples/Web-Sessions.ps1 @@ -0,0 +1,60 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with session persistent authentication. + +.DESCRIPTION + This script sets up a Pode server listening on port 8085 with session persistent authentication. + It demonstrates a simple server setup with a view counter. Each visit to the root URL ('http://localhost:8085') + increments the view counter stored in the session. + +.EXAMPLE + To run the sample: ./Web-Sessions.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/ -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-Sessions.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8085 +Start-PodeServer { + + # listen on localhost:8085 + Add-PodeEndpoint -Address localhost -Port 8085 -Protocol Http + + # set view engine to pode renderer + Set-PodeViewEngine -Type Pode + + # setup session details + Enable-PodeSessionMiddleware -Duration 120 -Extend -Generator { + return [System.IO.Path]::GetRandomFileName() + } + + # GET request for web page on "localhost:8085/" + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + $WebEvent.Session.Data.Views++ + Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @($WebEvent.Session.Data.Views); } + } + +} \ No newline at end of file diff --git a/examples/Web-Signal.ps1 b/examples/Web-Signal.ps1 new file mode 100644 index 000000000..8ecc63401 --- /dev/null +++ b/examples/Web-Signal.ps1 @@ -0,0 +1,74 @@ +<# +.SYNOPSIS + PowerShell script to set up a Pode server with various endpoints and error logging. + +.DESCRIPTION + This script sets up a Pode server that listens on a specified port and provides both HTTP and WebSocket + endpoints. It demonstrates how to set up WebSockets in Pode and logs errors and other request details + to the terminal. + +.PARAMETER Port + The port number on which the server will listen. Default is 8091. + +.EXAMPLE + To run the sample: ./Web-Signal.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/ -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-Signal.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening +Start-PodeServer -Threads 3 { + + # listen + Add-PodeEndpoint -Address localhost -Port 8091 -Protocol Http + Add-PodeEndpoint -Address localhost -Port 8091 -Protocol Ws + #Add-PodeEndpoint -Address localhost -Port 8090 -Certificate './certs/pode-cert.pfx' -CertificatePassword '1234' -Protocol Https + #Add-PodeEndpoint -Address localhost -Port 8091 -Certificate './certs/pode-cert.pfx' -CertificatePassword '1234' -Protocol Wss + + # log requests to the terminal + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging -Level Error, Debug, Verbose + + # set view engine to pode renderer + Set-PodeViewEngine -Type Html + + # GET request for web page + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Write-PodeViewResponse -Path 'websockets' + } + + # SIGNAL route, to return current date + Add-PodeSignalRoute -Path '/' -ScriptBlock { + $msg = $SignalEvent.Data.Message + + if ($msg -ieq '[date]') { + $msg = [datetime]::Now.ToString() + } + + Send-PodeSignal -Value @{ message = $msg } + } +} \ No newline at end of file diff --git a/examples/Web-SignalConnection.ps1 b/examples/Web-SignalConnection.ps1 new file mode 100644 index 000000000..aaeaf6b4e --- /dev/null +++ b/examples/Web-SignalConnection.ps1 @@ -0,0 +1,81 @@ +<# +.SYNOPSIS + Sample script to set up a Pode server with WebSocket support, listening on localhost. + +.DESCRIPTION + This script initializes a Pode server that listens on localhost:8081 with WebSocket support. + It includes logging to the terminal and several routes and timers for WebSocket connections. + The server can connect to a WebSocket from an external script and respond to messages. + +.PARAMETER ScriptPath + Path of the script being executed. + +.PARAMETER podePath + Path of the Pode module. + +.EXAMPLE + To run the sample: ./Web-SignalConnection.ps1 + Run this script to start the Pode server and navigate to 'http://localhost:8081' in your browser. + The server supports WebSocket connections and includes routes for connecting and resetting + WebSocket connections. + + Invoke-RestMethod -Uri http://localhost:8081/connect/ -Method Get + Invoke-RestMethod -Uri http://localhost:8081/reset/ -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-SignalConnection.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening +Start-PodeServer -EnablePool WebSockets { + + # listen + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + # log requests to the terminal + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging -Level Error, Debug, Verbose + + # connect to web socket from web-signal.ps1 + Connect-PodeWebSocket -Name 'Example' -Url 'ws://localhost:8091' -ScriptBlock { + $WsEvent.Data | Out-Default + if ($WsEvent.Data.message -inotlike '*Ex:*') { + Send-PodeWebSocket -Message @{ message = "Ex: $($WsEvent.Data.message)" } + } + } + + Add-PodeRoute -Method Get -Path '/connect' -ScriptBlock { + Connect-PodeWebSocket -Name 'Test' -Url 'wss://ws.ifelse.io/' -ScriptBlock { + $WsEvent.Request | out-default + } + } + + Add-PodeTimer -Name 'Test' -Interval 10 -ScriptBlock { + $rand = Get-Random -Minimum 10 -Maximum 1000 + Send-PodeWebSocket -Name 'Test' -Message "hello $rand" + } + + Add-PodeRoute -Method Get -Path '/reset' -ScriptBlock { + Reset-PodeWebSocket -Name 'Example' + } +} \ No newline at end of file diff --git a/examples/Web-SimplePages.ps1 b/examples/Web-SimplePages.ps1 new file mode 100644 index 000000000..acbff880a --- /dev/null +++ b/examples/Web-SimplePages.ps1 @@ -0,0 +1,54 @@ +<# +.SYNOPSIS + PowerShell script to set up a Pode server with various pages and error logging. + +.DESCRIPTION + This script sets up a Pode server that listens on a specified port and provides several routes + to display process and service information, as well as static views. + +.PARAMETER Port + The port number on which the server will listen. Default is 8081. + +.EXAMPLE + To run the sample: ./Web-SimplePages.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/Processes/ -Method Get + Invoke-RestMethod -Uri http://localhost:8081/Services/ -Method Get + Invoke-RestMethod -Uri http://localhost:8081/Index/ -Method Get + Invoke-RestMethod -Uri http://localhost:8081/File/ -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-SimplePages.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + Add-PodePage -Name Processes -ScriptBlock { Get-Process } + Add-PodePage -Name Services -ScriptBlock { Get-Service } + Add-PodePage -Name Index -View 'simple' + Add-PodePage -Name File -FilePath '.\views\simple.pode' -Data @{ 'numbers' = @(1, 2, 3); } + +} \ No newline at end of file diff --git a/examples/Web-Sockets.ps1 b/examples/Web-Sockets.ps1 new file mode 100644 index 000000000..e3f89f859 --- /dev/null +++ b/examples/Web-Sockets.ps1 @@ -0,0 +1,58 @@ +<# +.SYNOPSIS + PowerShell script to set up a Pode server with HTTPS and logging. + +.DESCRIPTION + This script sets up a Pode server that listens on a specified port with HTTPS protocol using a certificate. + It includes request logging and provides a sample route to return a JSON response. + +.PARAMETER Port + The port number on which the server will listen. Default is 8081. + +.EXAMPLE + To run the sample: ./Web-Sockets.ps1 + + Invoke-RestMethod -Uri https://localhost:8081/ -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-Sockets.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening +Start-PodeServer -Threads 5 { + + # listen + Add-PodeEndpoint -Address localhost -Port 8081 -Certificate './certs/pode-cert.pfx' -CertificatePassword '1234' -Protocol Https + # Add-PodeEndpoint -Address localhost -Port 8081 -SelfSigned -Protocol Https + + # log requests to the terminal + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + # GET request for web page on "localhost:8085/" + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Write-PodeJsonResponse -Value @{ + Kenobi = 'Hello, there' + } + } +} \ No newline at end of file diff --git a/examples/Web-Sse.ps1 b/examples/Web-Sse.ps1 new file mode 100644 index 000000000..188564b27 --- /dev/null +++ b/examples/Web-Sse.ps1 @@ -0,0 +1,69 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with Server-Sent Events (SSE) and logging. + +.DESCRIPTION + This script sets up a Pode server that listens on port 8081, logs errors to the terminal, and handles Server-Sent Events (SSE) connections. The server sends periodic SSE events and provides routes to interact with SSE connections. + +.EXAMPLE + To run the sample: ./Web-Sse.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/data -Method Get + Invoke-RestMethod -Uri http://localhost:8081/ -Method Get + Invoke-RestMethod -Uri http://localhost:8081/sse -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-Sse.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 3 { + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + # log errors + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging -Levels * + + # open local sse connection, and send back data + Add-PodeRoute -Method Get -Path '/data' -ScriptBlock { + ConvertTo-PodeSseConnection -Name 'Data' -Scope Local + Send-PodeSseEvent -Id 1234 -EventType Action -Data 'hello, there!' -FromEvent + Start-Sleep -Seconds 3 + Send-PodeSseEvent -Id 1337 -EventType BoldOne -Data 'general kenobi' -FromEvent + } + + # home page to get sse events + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Write-PodeViewResponse -Path 'sse-home' + } + + Add-PodeRoute -Method Get -Path '/sse' -ScriptBlock { + ConvertTo-PodeSseConnection -Name 'Test' + } + + Add-PodeTimer -Name 'SendEvent' -Interval 10 -ScriptBlock { + Send-PodeSseEvent -Name 'Test' -Data "An Event! $(Get-Random -Minimum 1 -Maximum 100)" + } +} \ No newline at end of file diff --git a/examples/Web-Static.ps1 b/examples/Web-Static.ps1 new file mode 100644 index 000000000..ded77ada1 --- /dev/null +++ b/examples/Web-Static.ps1 @@ -0,0 +1,74 @@ +<# +.SYNOPSIS + PowerShell script to set up a Pode server with various routes for static assets and view responses. + +.DESCRIPTION + This script sets up a Pode server that listens on a specified port, logs requests and errors to the terminal, + and serves static assets as well as view responses using the Pode view engine. + +.PARAMETER Port + The port number on which the server will listen. Default is 8081. + +.EXAMPLE + To run the sample: ./Web-Static.ps1 + + Connect by browser to: + http://localhost:8081/ + http://localhost:8081/download + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-Static.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +param( + [int] + $Port = 8081 +) + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port $port -Protocol Http + New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + # set view engine to pode renderer + Set-PodeViewEngine -Type Pode + + # STATIC asset folder route + Add-PodeStaticRoute -Path '/assets' -Source './assets' -Defaults @('index.html') + Add-PodeStaticRoute -Path '/assets/download' -Source './assets' -DownloadOnly + + # GET request for web page on "localhost:8081/" + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Write-PodeViewResponse -Path 'web-static' -Data @{ 'numbers' = @(1, 2, 3); } + } + + # GET request to download a file from static route + Add-PodeRoute -Method Get -Path '/download' -ScriptBlock { + Set-PodeResponseAttachment -Path '/assets/images/Fry.png' + } + +} \ No newline at end of file diff --git a/examples/Web-StaticAuth.ps1 b/examples/Web-StaticAuth.ps1 new file mode 100644 index 000000000..2ff513681 --- /dev/null +++ b/examples/Web-StaticAuth.ps1 @@ -0,0 +1,86 @@ +<# +.SYNOPSIS + PowerShell script to set up a Pode server with basic authentication and various routes for static assets and view responses. + +.DESCRIPTION + This script sets up a Pode server that listens on a specified port, logs requests and errors to the terminal, + and serves static assets as well as view responses using the Pode view engine. It also includes basic authentication + for accessing static assets. + +.PARAMETER Port + The port number on which the server will listen. Default is 8081. + +.EXAMPLE + To run the sample: ./Web-StaticAuth.ps1 + + Connect by browser to: + http://localhost:8081/ + http://localhost:8081/download + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-StaticAuth.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + # setup basic auth (base64> username:password in header) + New-PodeAuthScheme -Basic -Realm 'Pode Static Page' | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { + param($username, $password) + + # here you'd check a real user storage, this is just for example + if ($username -eq 'morty' -and $password -eq 'pickle') { + return @{ + User = @{ + ID ='M0R7Y302' + Name = 'Morty' + Type = 'Human' + } + } + } + + return @{ Message = 'Invalid details supplied' } + } + + # set view engine to pode renderer + Set-PodeViewEngine -Type Pode + + # STATIC asset folder route + Add-PodeStaticRoute -Path '/assets' -Source './assets' -Defaults @('index.html') -Authentication 'Validate' + Add-PodeStaticRoute -Path '/assets/download' -Source './assets' -DownloadOnly + + # GET request for web page on "localhost:8081/" + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Write-PodeViewResponse -Path 'web-static' -Data @{ 'numbers' = @(1, 2, 3); } + } + + # GET request to download a file from static route + Add-PodeRoute -Method Get -Path '/download' -ScriptBlock { + Set-PodeResponseAttachment -Path '/assets/images/Fry.png' + } + +} \ No newline at end of file diff --git a/examples/Web-TpEPS.ps1 b/examples/Web-TpEPS.ps1 new file mode 100644 index 000000000..7d013673d --- /dev/null +++ b/examples/Web-TpEPS.ps1 @@ -0,0 +1,63 @@ +<# +.SYNOPSIS + PowerShell script to set up a Pode server with EPS view engine. + +.DESCRIPTION + This script sets up a Pode server that listens on a specified port, logs requests to the terminal, + and uses the EPS renderer for serving web pages. + +.EXAMPLE + To run the sample: ./Web-TpEPS.ps1 + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-TpEPS.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } + + Import-Module -Name 'EPS' -ErrorAction Stop +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + # log requests to the terminal + New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging + + # set view engine to EPS renderer + Set-PodeViewEngine -Type EPS -ScriptBlock { + param($path, $data) + $template = Get-Content -Path $path -Raw -Force + + if ($null -eq $data) { + return (Invoke-EpsTemplate -Template $template) + } + else { + return (Invoke-EpsTemplate -Template $template -Binding $data) + } + } + + # GET request for web page on "localhost:8081/" + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Write-PodeViewResponse -Path 'index' -Data @{ 'numbers' = @(1, 2, 3); 'date' = [DateTime]::UtcNow; } + } + +} \ No newline at end of file diff --git a/examples/Web-TpPSHTML.ps1 b/examples/Web-TpPSHTML.ps1 new file mode 100644 index 000000000..e187d76cb --- /dev/null +++ b/examples/Web-TpPSHTML.ps1 @@ -0,0 +1,56 @@ +<# +.SYNOPSIS + PowerShell script to set up a Pode server with PSHTML view engine. + +.DESCRIPTION + This script sets up a Pode server that listens on a specified port, logs requests to the terminal, + and uses the PSHTML renderer for serving web pages. + +.EXAMPLE + To run the sample: ./Web-TpPSHTML.ps1 + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-TpPSHTML.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } + Import-Module -Name PSHTML -ErrorAction Stop +} +catch { throw } + +# or just: +# Import-Module Pode + + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + # log requests to the terminal + New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging + + # set view engine to PSHTML renderer + Set-PodeViewEngine -Type PSHTML -Extension PS1 -ScriptBlock { + param($path, $data) + return [string](. $path $data) + } + + # GET request for web page on "localhost:8081/" + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Write-PodeViewResponse -Path 'index' -Data @{ 'numbers' = @(1, 2, 3); } + } + +} \ No newline at end of file diff --git a/examples/Web-Upload.ps1 b/examples/Web-Upload.ps1 new file mode 100644 index 000000000..9f022f14e --- /dev/null +++ b/examples/Web-Upload.ps1 @@ -0,0 +1,82 @@ +<# +.SYNOPSIS + PowerShell script to set up a Pode server with file upload routes. + +.DESCRIPTION + This script sets up a Pode server that listens on a specified port and provides routes + for uploading single and multiple files. + +.PARAMETER Port + Specifies the port on which the server will listen. Default is 8081. + +.EXAMPLE + To run the sample: ./Web-Upload.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/upload -Method Post + Invoke-RestMethod -Uri http://localhost:8081/ -Method Get + + Invoke-RestMethod -Uri http://localhost:8081/upload-multi -Method Post + Invoke-RestMethod -Uri http://localhost:8081/multi -Method Get + +#.LINK doesn't work + https://github.com/Badgerati/Pode/blob/develop/examples/Web-Upload.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +param( + [int] + $Port = 8081 +) + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port $port -Protocol Http + + Set-PodeViewEngine -Type HTML + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + # GET request for web page on "localhost:8081/" + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Write-PodeViewResponse -Path 'web-upload' + } + + # POST request to upload a file + Add-PodeRoute -Method Post -Path '/upload' -ScriptBlock { + Save-PodeRequestFile -Key 'avatar' + Move-PodeResponseUrl -Url '/' + } + + # GET request for web page on "localhost:8081/multi" + Add-PodeRoute -Method Get -Path '/multi' -ScriptBlock { + Write-PodeViewResponse -Path 'web-upload-multi' + } + + # POST request to upload multiple files + Add-PodeRoute -Method Post -Path '/upload-multi' -ScriptBlock { + Save-PodeRequestFile -Key 'avatar' -Path 'C:/temp' -FileName 'Ruler.png' + Move-PodeResponseUrl -Url '/multi' + } + +} \ No newline at end of file diff --git a/examples/Web-UploadKestrel.ps1 b/examples/Web-UploadKestrel.ps1 new file mode 100644 index 000000000..8817a090f --- /dev/null +++ b/examples/Web-UploadKestrel.ps1 @@ -0,0 +1,66 @@ +<# +.SYNOPSIS + PowerShell script to set up a Pode server with Kestrel listener and file upload functionality. + +.DESCRIPTION + This script sets up a Pode server that listens on a specified port using the Kestrel listener type. + It serves a web page for file upload and processes file uploads, saving them to the server. + +.PARAMETER Port + The port number on which the server will listen. Default is 8081. + +.EXAMPLE + To run the sample: ./Web-UploadKestrel.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/upload -Method Post + Invoke-RestMethod -Uri http://localhost:8081/ -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-UploadKestrel.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +param( + [int] + $Port = 8081 +) + +try { + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } + Import-Module -Name Pode.Kestrel -ErrorAction Stop +} +catch { throw } + + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 -ListenerType Kestrel { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port $port -Protocol Http + + Set-PodeViewEngine -Type HTML + + # GET request for web page on "localhost:8081/" + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Write-PodeViewResponse -Path 'web-upload' + } + + # POST request to upload a file + Add-PodeRoute -Method Post -Path '/upload' -ScriptBlock { + Save-PodeRequestFile -Key 'avatar' + Move-PodeResponseUrl -Url '/' + } + +} \ No newline at end of file diff --git a/examples/WebAuth-ApikeyJWT.ps1 b/examples/WebAuth-ApikeyJWT.ps1 new file mode 100644 index 000000000..dcf242d54 --- /dev/null +++ b/examples/WebAuth-ApikeyJWT.ps1 @@ -0,0 +1,106 @@ +<# +.SYNOPSIS + A PowerShell script to set up a Pode server with JWT authentication and various route configurations. + +.DESCRIPTION + This script sets up a Pode server that listens on a specified port, enables request and error logging, + and configures JWT authentication. It also defines a route to fetch a list of users, requiring authentication. + +.PARAMETER Location + The location where the API key is expected. Valid values are 'Header', 'Query', and 'Cookie'. Default is 'Header'. + + .NOTES + ------------- + None Signed + Req: Invoke-RestMethod -Uri 'http://localhost:8081/users' -Headers @{ 'X-API-KEY' = 'eyJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6Im1vcnR5Iiwic3ViIjoiMTIzIn0.' } + ------------- + + ------------- + Signed + Req: Invoke-RestMethod -Uri 'http://localhost:8081/users' -Headers @{ 'X-API-KEY' = 'eyJhbGciOiJoczI1NiJ9.eyJ1c2VybmFtZSI6Im1vcnR5Iiwic3ViIjoiMTIzIn0.WIOvdwk4mNrNC9EtTcQccmLHJc02gAuonXClHMFOjKM' } + + (add -Secret 'secret' to New-PodeAuthScheme below) + + ------------- + +.EXAMPLE + To run the sample: ./WebAuth-ApikeyJWT.ps1 + + Invoke-RestMethod -Uri http://localhost:8081/users -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/WebAuth-ApikeyJWT.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +param( + [Parameter()] + [ValidateSet('Header', 'Query', 'Cookie')] + [string] + $Location = 'Header' +) + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer -Threads 2 { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + New-PodeLoggingMethod -File -Name 'requests' | Enable-PodeRequestLogging + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + # setup bearer auth + New-PodeAuthScheme -ApiKey -Location $Location -AsJWT | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { + param($jwt) + + # here you'd check a real user storage, this is just for example + if ($jwt.username -ieq 'morty') { + return @{ + User = @{ + ID = 'M0R7Y302' + Name = 'Morty' + Type = 'Human' + } + } + } + + return $null + } + + # GET request to get list of users (since there's no session, authentication will always happen) + Add-PodeRoute -Method Get -Path '/users' -Authentication 'Validate' -ScriptBlock { + Write-PodeJsonResponse -Value @{ + Users = @( + @{ + Name = 'Deep Thought' + Age = 42 + }, + @{ + Name = 'Leeroy Jenkins' + Age = 1337 + } + ) + } + } + +} \ No newline at end of file diff --git a/examples/WebHook.ps1 b/examples/WebHook.ps1 new file mode 100644 index 000000000..8503dab17 --- /dev/null +++ b/examples/WebHook.ps1 @@ -0,0 +1,203 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a webhook server using Pode. + +.DESCRIPTION + This script sets up a webhook server using Pode. It includes endpoints for storing data, + subscribing to webhook events, and unsubscribing from webhook events. It also defines + an OpenAPI specification for the server and enables Swagger documentation. + +.EXAMPLE + To run the sample: ./WebHook.ps1 + + OpenAPI Info: + Specification: http://localhost:8081/docs/openapi/v3.1 + Documentation: http://localhost:8081/docs/v3.1 + + Invoke-RestMethod -Uri http://localhost:8081/store -Method Post -Header @{ + 'accept' = 'application/json' + 'Content-Type' = 'application/json' + } -Body (@{ "value"= "arm";"key"="cpu"}| ConvertTo-Json) + + Invoke-RestMethod -Uri http://localhost:8081/subscribe -Method Post -Header @{ + 'accept' = 'application/json' + 'Content-Type' = 'application/json' + } -Body (@{ url = 'http://example.com/webhook' } | ConvertTo-Json) + + Invoke-RestMethod -Uri http://localhost:8081/unsubscribe -Method Post -Header @{ + 'accept' = 'application/json' + 'Content-Type' = 'application/json' + } -Body (@{ url = 'http://example.com/webhook' } | ConvertTo-Json) + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/WebHook.ps1 +.NOTES + Author: Pode Team + License: MIT License + +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# Define the server configuration +Start-PodeServer { + # Listen on port 8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + # Initialize a hashtable to store subscriptions + Set-PodeState -Name 'subscriptions' -Value @{} | Out-Null + + # Enable terminal logging for errors + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + # Enable OpenAPI documentation + Enable-PodeOpenApi -Path '/docs/openapi/v3.1' -OpenApiVersion '3.1.0' -DisableMinimalDefinitions -NoDefaultResponses -EnableSchemaValidation + Add-PodeOAInfo -Title 'WebHook' -Version 1.0.0 -Description 'Webhook Sample' -LicenseName 'MIT License' -LicenseUrl 'https://github.com/Badgerati/Pode?tab=MIT-1-ov-file#readme' + + # Enable Swagger viewer for the OpenAPI documentation + Enable-PodeOAViewer -Type Swagger -Path '/docs/v3.1/swagger' + Enable-PodeOAViewer -Bookmarks -Path '/docs/v3.1' + + # Define OpenAPI components + # Define WebhookPayload schema + New-PodeOAStringProperty -Name 'key' -Description 'Index key' -Example 'cpu' -Required | + New-PodeOAStringProperty -Name 'value' -Description 'Value' -Example 'arm' -Required | + New-PodeOAObjectProperty -NoAdditionalProperties | Add-PodeOAComponentSchema -Name 'WebhookPayload' + + # Define WebhookUri schema + New-PodeOAStringProperty -Name 'url' -Format Uri -Example 'http://example.com/webhook' | New-PodeOAObjectProperty | Add-PodeOAComponentSchema -Name 'WebhookUri' + + # Define responses + Add-PodeOAComponentResponse -Name 'SucessfulResponse' -Description 'Successful operation' -ContentSchemas (@{'application/json' = (New-PodeOAStringProperty -Name 'message' -Example 'Operation completed successfully!' | New-PodeOAObjectProperty) }) + Add-PodeOAComponentResponse -Name 'FailureResponse' -Description 'Failed operation' -ContentSchemas (@{'application/json' = (New-PodeOAStringProperty -Name 'message' -Example 'Operation Failed!' | New-PodeOAObjectProperty) }) + + # Route for subscribing to the webhook + Add-PodeRoute -Method Post -Path '/subscribe' -ScriptBlock { + $contentType = Get-PodeHeader -Name 'Content-Type' + switch ($contentType) { + 'application/json' { + $data = $WebEvent.data + + # Validate the incoming request against the WebhookUri schema + $Validate = Test-PodeOAJsonSchemaCompliance -Json $data -SchemaReference 'WebhookUri' + if ($Validate.result) { + # Add the subscription (assuming 'url' is the endpoint to subscribe) + Lock-PodeObject -ScriptBlock { + if ($state:subscriptions.ContainsKey($data.url)) { + Write-PodeJsonResponse -Value @{ message = 'Subscription already exist.' } -StatusCode 400 + } + else { + $state:subscriptions[$data.url] = $true + # Respond with a status message + Write-PodeJsonResponse -Value @{ message = 'Subscribed successfully!' } + } + } + } + else { + Write-PodeJsonResponse -StatusCode 400 -Value @{ + message = $Validate.message -join ', ' + } + } + } + default { + Write-PodeJsonResponse -Value @{ message = 'Invalid Content-Type' } -StatusCode 400 + } + } + } -PassThru | Set-PodeOARouteInfo -Summary 'Subscribe to webhook events' -OperationId 'subscribe' -Description 'Allows a client to subscribe to webhook events.' -PassThru | + Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Required -Content @{ 'application/json' = 'WebhookUri' }) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Reference 'SucessfulResponse' -PassThru | + Add-PodeOAResponse -StatusCode 400 -Reference 'FailureResponse' + + # Route for unsubscribing from the webhook + Add-PodeRoute -Method Post -Path '/unsubscribe' -ScriptBlock { + $contentType = Get-PodeHeader -Name 'Content-Type' + switch ($contentType) { + 'application/json' { + $data = $WebEvent.data + + # Validate the incoming request against the WebhookUri schema + $Validate = Test-PodeOAJsonSchemaCompliance -Json $data -SchemaReference 'WebhookUri' + if ($Validate.result) { + # Remove the subscription + Lock-PodeObject -ScriptBlock { + if ($state:subscriptions.ContainsKey($data.url)) { + $state:subscriptions.Remove($data.url) + # Respond with a status message + Write-PodeJsonResponse -Value @{ message = 'Unsubscribed successfully!' } -StatusCode 200 + } + else { + Write-PodeJsonResponse -Value @{ message = "Subscription doesn't exist." } -StatusCode 400 + } + } + } + else { + Write-PodeJsonResponse -StatusCode 400 -Value @{ + message = $Validate.message -join ', ' + } + } + } + default { + Write-PodeJsonResponse -Value @{ message = 'Invalid Content-Type' } -StatusCode 400 + } + } + } -PassThru | Set-PodeOARouteInfo -Summary 'Unsubscribe from webhook events' -OperationId 'unsubscribe' -Description 'Allows a client to unsubscribe from webhook events.' -PassThru | + Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Required -Content @{ 'application/json' = 'WebhookUri' }) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Reference 'SucessfulResponse' -PassThru | + Add-PodeOAResponse -StatusCode 400 -Reference 'FailureResponse' + + # Route for handling data storage via POST + Add-PodeRoute -Method Post -Path '/store' -ScriptBlock { + $data = $WebEvent.data + # Log the received data to the console + Write-PodeHost 'Received Store Data:' -NoNewLine + Write-PodeHost $data -Explode + + # Validate the incoming request against the WebhookPayload schema + $Validate = Test-PodeOAJsonSchemaCompliance -Json $data -SchemaReference 'WebhookPayload' + if ($Validate.result) { + Lock-PodeObject -ScriptBlock { + # Notify all subscribed endpoints + foreach ($url in $state:subscriptions.Keys) { + try { + Write-PodeHost "Notifying $url" + Invoke-RestMethod -Uri $url -Method Post -Body ($data | ConvertTo-Json) -ContentType 'application/json' + } + catch { + Write-PodeHost "Failed to notify $url" + } + } + } + # Respond with a status message + Write-PodeJsonResponse -Value @{ message = 'Webhook processed successfully!' } + } + else { + Write-PodeJsonResponse -StatusCode 400 -Value @{ + message = $Validate.message -join ', ' + } + } + } -PassThru | Set-PodeOARouteInfo -Summary 'Store data' -OperationId 'storeData' -Description 'Endpoint for storing data.' -PassThru | + Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Required -Content @{ 'application/json' = 'WebhookPayload' }) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Reference 'SucessfulResponse' -PassThru | + Add-PodeOAResponse -StatusCode 400 -Reference 'FailureResponse' + + # Define the webhook event for OpenAPI documentation + Add-PodeOAWebhook -Name 'webhookEvent' -Method Post -PassThru | + Set-PodeOARouteInfo -Summary 'Handle webhook event' -Description 'Endpoint for receiving webhook events.' -OperationId 'handleWebhookEvent' -PassThru | + Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Required -Content @{ 'application/json' = 'WebhookPayload' }) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Reference 'SucessfulResponse' -PassThru | + Add-PodeOAResponse -StatusCode 400 -Description 'Invalid input' -PassThru + +} diff --git a/examples/WebNunit-RestApi.ps1 b/examples/WebNunit-RestApi.ps1 new file mode 100644 index 000000000..06614204a --- /dev/null +++ b/examples/WebNunit-RestApi.ps1 @@ -0,0 +1,90 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server for running NUnit tests. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081 with a POST endpoint for running NUnit tests. + It accepts JSON data specifying the test DLL path and the tests to run, executes the tests using NUnit, + and returns the test results as an XML response. + +.EXAMPLE + To run the sample: ./Nunit-RestApi.ps1 + + A typical request to "localhost:8087/api/nunit/run-rest" looks as follows: + + { + "dll": "/path/test.dll", + "tests": [ + "Test.Tests.Method1" + ], + "categories": {} + } + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/WebNunit-RestApi.ps1 +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8081 +Start-PodeServer { + + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + # post endpoint, that accepts test to run, and path to test dll + Add-PodeRoute -Method Post -Path '/api/nunit/run-test' -ScriptBlock { + # general + $date = [DateTime]::UtcNow.ToString('yyyy-MM-dd_HH-mm-ss-fffff') + $data = $WebEvent.Data + + # get data passed in + $dll = $data.dll + $tests = $data.tests -join ',' + $catsInclude = $data.categories.include -join ',' + $catsExclude = $data.categories.exclude -join ',' + $results = "$($date).xml" + $outputs = "$($date).txt" + $tool = 'C:\Program Files (x86)\NUnit 2.6.4\bin\nunit-console.exe' + + # run the tests + if (![string]::IsNullOrWhiteSpace($catsInclude)) + { + $catsInclude = "/include=$($catsInclude)" + } + + if (![string]::IsNullOrWhiteSpace($catsExclude)) + { + $catsExclude = "/exclude=$($catsExclude)" + } + + $_args = "/result=$($results) /out=$($outputs) $($catsInclude) $($catsExclude) /run=$($tests) /nologo /nodots `"$($dll)`"" + Start-Process -FilePath $tool -NoNewWindow -Wait -ArgumentList $_args -ErrorAction Stop | Out-Null + + # return results + Write-PodeXmlResponse -Path $results + + # delete results file + Remove-Item -Path $results -Force | Out-Null + Remove-Item -Path $outputs -Force | Out-Null + } + +} \ No newline at end of file diff --git a/examples/Write-Response.ps1 b/examples/Write-Response.ps1 new file mode 100644 index 000000000..c22fff577 --- /dev/null +++ b/examples/Write-Response.ps1 @@ -0,0 +1,165 @@ +<# +.SYNOPSIS + PowerShell script to set up a Pode server with various routes for different response types. + +.DESCRIPTION + This script sets up a Pode server that listens on a specified port and provides various routes to + retrieve process information in different formats (HTML, Text, CSV, JSON, XML, YAML). + +.EXAMPLE + To run the sample: ./Write-Response.ps1 + + # HTML responses + Invoke-RestMethod -Uri http://localhost:8081/html/processes -Method Get + Invoke-RestMethod -Uri http://localhost:8081/html/processesPiped -Method Get + + # Text responses + Invoke-RestMethod -Uri http://localhost:8081/text/processes -Method Get + Invoke-RestMethod -Uri http://localhost:8081/text/processesPiped -Method Get + + # CSV responses + Invoke-RestMethod -Uri http://localhost:8081/csv/processes -Method Get + Invoke-RestMethod -Uri http://localhost:8081/csv/processesPiped -Method Get + Invoke-RestMethod -Uri http://localhost:8081/csv/string -Method Get + Invoke-RestMethod -Uri http://localhost:8081/csv/hash -Method Get + + # JSON responses + Invoke-RestMethod -Uri http://localhost:8081/json/processes -Method Get + Invoke-RestMethod -Uri http://localhost:8081/json/processesPiped -Method Get + + # XML responses + Invoke-RestMethod -Uri http://localhost:8081/xml/processes -Method Get + Invoke-RestMethod -Uri http://localhost:8081/xml/processesPiped -Method Get + Invoke-RestMethod -Uri http://localhost:8081/xml/hash -Method Get + + # YAML responses + Invoke-RestMethod -Uri http://localhost:8081/yaml/processes -Method Get + Invoke-RestMethod -Uri http://localhost:8081/yaml/processesPiped -Method Get + + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Write-Response.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +Start-PodeServer { + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + # nopipe + Add-PodeRoute -Path '/html/processes' -Method Get -ScriptBlock { + $myProcess = Get-Process | .{ process { if ($_.WS -gt 100mb) { $_ } } } | + Select-Object Name, @{e = { [int]($_.WS / 1mb) }; n = 'WS' } | + Sort-Object WS -Descending + Write-PodeHtmlResponse -Value $myProcess -StatusCode 200 + } + # pipe + Add-PodeRoute -Path '/html/processesPiped' -Method Get -ScriptBlock { + Get-Process | .{ process { if ($_.WS -gt 100mb) { $_ } } } | + Select-Object Name, @{e = { [int]($_.WS / 1mb) }; n = 'WS' } | + Sort-Object WS -Descending | Write-PodeHtmlResponse -StatusCode 200 + } + + # nopipe + Add-PodeRoute -Path '/text/processes' -Method Get -ScriptBlock { + $myProcess = Get-Process | .{ process { if ($_.WS -gt 100mb) { $_ } } } | + Select-Object Name, @{e = { [int]($_.WS / 1mb) }; n = 'WS' } | + Sort-Object WS -Descending | Out-String + Write-PodeTextResponse -Value $myProcess -StatusCode 200 + } + # pipe + Add-PodeRoute -Path '/text/processesPiped' -Method Get -ScriptBlock { + Get-Process | .{ process { if ($_.WS -gt 100mb) { $_ } } } | + Select-Object Name, @{e = { [int]($_.WS / 1mb) }; n = 'WS' } | + Sort-Object WS -Descending | Out-String | Write-PodeTextResponse -StatusCode 200 + } + + # nopipe + Add-PodeRoute -Path '/csv/processes' -Method Get -ScriptBlock { + $myProcess = Get-Process | .{ process { if ($_.WS -gt 100mb) { $_ } } } | + Select-Object Name, @{e = { [int]($_.WS / 1mb) }; n = 'WS' } | + Sort-Object WS -Descending + Write-PodeCsvResponse -Value $myProcess -StatusCode 200 + } + + # pipe + Add-PodeRoute -Path '/csv/processesPiped' -Method Get -ScriptBlock { + Get-Process | .{ process { if ($_.WS -gt 100mb) { $_ } } } | + Select-Object Name, @{e = { [int]($_.WS / 1mb) }; n = 'WS' } | + Sort-Object WS -Descending | Write-PodeCsvResponse -StatusCode 200 + } + + Add-PodeRoute -Path '/csv/string' -Method Get -ScriptBlock { + Write-PodeCsvResponse -Value "Name`nRick`nDon" + } + + Add-PodeRoute -Path '/csv/hash' -Method Get -ScriptBlock { + Write-PodeCsvResponse -Value @(@{ Name = 'Rick' }, @{ Name = 'Don' }) + } + + # nopipe + Add-PodeRoute -Path '/json/processes' -Method Get -ScriptBlock { + $myProcess = Get-Process | .{ process { if ($_.WS -gt 100mb) { $_ } } } | + Select-Object Name, @{e = { [int]($_.WS / 1mb) }; n = 'WS' } | + Sort-Object WS -Descending + Write-PodeJsonResponse -Value $myProcess -StatusCode 200 + } + + # pipe + Add-PodeRoute -Path '/json/processesPiped' -Method Get -ScriptBlock { + Get-Process | .{ process { if ($_.WS -gt 100mb) { $_ } } } | + Select-Object Name, @{e = { [int]($_.WS / 1mb) }; n = 'WS' } | + Sort-Object WS -Descending | Write-PodeJsonResponse -StatusCode 200 + } + + # nopipe + Add-PodeRoute -Path '/xml/processes' -Method Get -ScriptBlock { + $myProcess = Get-Process | .{ process { if ($_.WS -gt 100mb) { $_ } } } | + Select-Object Name, @{e = { [int]($_.WS / 1mb) }; n = 'WS' } | + Sort-Object WS -Descending + Write-PodeXmlResponse -Value $myProcess -StatusCode 200 + } + + # pipe + Add-PodeRoute -Path '/xml/processesPiped' -Method Get -ScriptBlock { + Get-Process | .{ process { if ($_.WS -gt 100mb) { $_ } } } | + Select-Object Name, @{e = { [int]($_.WS / 1mb) }; n = 'WS' } | + Sort-Object WS -Descending | Write-PodeXmlResponse -StatusCode 200 + } + + + Add-PodeRoute -Path '/xml/hash' -Method Get -ScriptBlock { + Write-PodeXmlResponse -Value @(@{ Name = 'Rick' }, @{ Name = 'Don' }) + } + + # nopipe + Add-PodeRoute -Path '/yaml/processes' -Method Get -ScriptBlock { + $myProcess = Get-Process | .{ process { if ($_.WS -gt 100mb) { $_ } } } | + Select-Object Name, @{e = { [int]($_.WS / 1mb) }; n = 'WS' } | + Sort-Object WS -Descending + Write-PodeYamlResponse -Value $myProcess -StatusCode 200 -ContentType 'text/yaml' + } + + # pipe + Add-PodeRoute -Path '/yaml/processesPiped' -Method Get -ScriptBlock { + Get-Process | .{ process { if ($_.WS -gt 100mb) { $_ } } } | + Select-Object Name, @{e = { [int]($_.WS / 1mb) }; n = 'WS' } | + Sort-Object WS -Descending | Write-PodeYamlResponse -StatusCode 200 -ContentType 'text/yaml' + } + +} \ No newline at end of file diff --git a/examples/caching.ps1 b/examples/caching.ps1 deleted file mode 100644 index 36c8a6172..000000000 --- a/examples/caching.ps1 +++ /dev/null @@ -1,64 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening on port 8085 -Start-PodeServer -Threads 3 { - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8090 -Protocol Http - - # log errors - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - Set-PodeCacheDefaultTtl -Value 60 - - $params = @{ - Set = { - param($key, $value, $ttl) - $null = redis-cli -h localhost -p 6379 SET $key "$($value)" EX $ttl - } - Get = { - param($key, $metadata) - $result = redis-cli -h localhost -p 6379 GET $key - $result = [System.Management.Automation.Internal.StringDecorated]::new($result).ToString('PlainText') - if ([string]::IsNullOrEmpty($result) -or ($result -ieq '(nil)')) { - return $null - } - return $result - } - Test = { - param($key) - $result = redis-cli -h localhost -p 6379 EXISTS $key - return [System.Management.Automation.Internal.StringDecorated]::new($result).ToString('PlainText') - } - Remove = { - param($key) - $null = redis-cli -h localhost -p 6379 EXPIRE $key -1 - } - Clear = {} - } - Add-PodeCacheStorage -Name 'Redis' @params - - # set default value for cache - $cache:cpu = (Get-Random -Minimum 1 -Maximum 1000) - - # get cpu, and cache it - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - if ((Test-PodeCache -Key 'cpu') -and ($null -ne $cache:cpu)) { - Write-PodeJsonResponse -Value @{ CPU = $cache:cpu } - # Write-PodeHost 'here - cached' - return - } - - # $cache:cpu = (Get-Counter '\Processor(_Total)\% Processor Time').CounterSamples.CookedValue - Start-Sleep -Milliseconds 500 - $cache:cpu = (Get-Random -Minimum 1 -Maximum 1000) - Write-PodeJsonResponse -Value @{ CPU = $cache:cpu } - # $cpu = (Get-Random -Minimum 1 -Maximum 1000) - # Write-PodeJsonResponse -Value @{ CPU = $cpu } - # Write-PodeHost 'here - raw' - } - -} \ No newline at end of file diff --git a/examples/create-routes.ps1 b/examples/create-routes.ps1 deleted file mode 100644 index 8ae84cd7c..000000000 --- a/examples/create-routes.ps1 +++ /dev/null @@ -1,34 +0,0 @@ - -#crete routes using different approaches -$ScriptPath=Split-Path -Parent -Path $MyInvocation.MyCommand.Path -$path = Split-Path -Parent -Path $ScriptPath -if (Test-Path -Path "$($path)/src/Pode.psm1" -PathType Leaf) { - Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop -} else { - Import-Module -Name 'Pode' -} - - -Start-PodeServer -Threads 1 -ScriptBlock { - Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - Add-PodeRoute -PassThru -Method Get -Path '/routeCreateScriptBlock/:id' -ScriptBlock ([ScriptBlock]::Create( (Get-Content -Path "$ScriptPath\scripts\routeScript.ps1" -Raw))) | - Set-PodeOARouteInfo -Summary 'Test' -OperationId 'routeCreateScriptBlock' -PassThru | - Set-PodeOARequest -Parameters @((New-PodeOAStringProperty -Name 'id' | ConvertTo-PodeOAParameter -In Path -Required) ) - - - Add-PodeRoute -PassThru -Method Post -Path '/routeFilePath/:id' -FilePath '.\scripts\routeScript.ps1' | Set-PodeOARouteInfo -Summary 'Test' -OperationId 'routeFilePath' -PassThru | - Set-PodeOARequest -Parameters @((New-PodeOAStringProperty -Name 'id' | ConvertTo-PodeOAParameter -In Path -Required) ) - - - Add-PodeRoute -PassThru -Method Get -Path '/routeScriptBlock/:id' -ScriptBlock { $Id = $WebEvent.Parameters['id'] ; Write-PodeJsonResponse -StatusCode 200 -Value @{'id' = $Id } } | - Set-PodeOARouteInfo -Summary 'Test' -OperationId 'routeScriptBlock' -PassThru | - Set-PodeOARequest -Parameters @((New-PodeOAStringProperty -Name 'id' | ConvertTo-PodeOAParameter -In Path -Required) ) - - - Add-PodeRoute -PassThru -Method Get -Path '/routeScriptSameScope/:id' -ScriptBlock { . $ScriptPath\scripts\routeScript.ps1 } | - Set-PodeOARouteInfo -Summary 'Test' -OperationId 'routeScriptSameScope' -PassThru | - Set-PodeOARequest -Parameters @((New-PodeOAStringProperty -Name 'id' | ConvertTo-PodeOAParameter -In Path -Required) ) - -} \ No newline at end of file diff --git a/examples/docker-compose.yml b/examples/docker-compose.yml index 2ac922cd5..1011c72a0 100644 --- a/examples/docker-compose.yml +++ b/examples/docker-compose.yml @@ -4,6 +4,6 @@ services: web: build: . ports: - - "8085:8085" + - "8081:8081" container_name: pode-example restart: always diff --git a/examples/dot-source-script.ps1 b/examples/dot-source-script.ps1 deleted file mode 100644 index e27ba14db..000000000 --- a/examples/dot-source-script.ps1 +++ /dev/null @@ -1,13 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# runs the logic once, then exits -Start-PodeServer { - - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - Use-PodeScript -Path './modules/script1.ps1' - -} diff --git a/examples/external-funcs.ps1 b/examples/external-funcs.ps1 deleted file mode 100644 index 0ae35a53d..000000000 --- a/examples/external-funcs.ps1 +++ /dev/null @@ -1,21 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# include the external function module -Import-PodeModule -Path './modules/external-funcs.psm1' - -# create a server, and start listening on port 8085 -Start-PodeServer { - - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http - - # GET request for "localhost:8085/" - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - Write-PodeJsonResponse -Value @{ 'result' = (Get-Greeting) } - } - -} \ No newline at end of file diff --git a/examples/file-monitoring.ps1 b/examples/file-monitoring.ps1 deleted file mode 100644 index 6d3a054f9..000000000 --- a/examples/file-monitoring.ps1 +++ /dev/null @@ -1,18 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server listening on port 8085, set to monitor file changes and restart the server -Start-PodeServer { - - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http - Set-PodeViewEngine -Type Pode - - # GET request for web page on "localhost:8085/" - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(1, 2, 3); } - } - -} diff --git a/examples/file-watchers.ps1 b/examples/file-watchers.ps1 deleted file mode 100644 index 06d655f79..000000000 --- a/examples/file-watchers.ps1 +++ /dev/null @@ -1,34 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening on port 9000 -Start-PodeServer -Verbose { - - # add two endpoints - # Add-PodeEndpoint -Address * -Port 9000 -Protocol Http - - # enable logging - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - # watchers - # Add-PodeFileWatcher -Name 'Test' -Path './public' -Include '*.txt', '*.md' -ScriptBlock { - # "[$($FileEvent.Type)]: $($FileEvent.FullPath)" | Out-Default - # } - - Add-PodeFileWatcher -Path 'C:/Projects/:project/src' -Include '*.ps1' -ScriptBlock { - "[$($FileEvent.Type)][$($FileEvent.Parameters['project'])]: $($FileEvent.FullPath)" | Out-Default - } - - # Add-PodeTimer -Name 'Test' -Interval 10 -ScriptBlock { - # $root = Get-PodeServerPath - # $file = Join-Path $root 'myfile.txt' - # 'hi!' | Out-File -FilePath $file -Append -Force - # } - - # Add-PodeFileWatcher -Path '.' -Include '*.txt' -ScriptBlock { - # "[$($FileEvent.Type)]: $($FileEvent.FullPath)" | Out-Default - # } -} \ No newline at end of file diff --git a/examples/iis-example.ps1 b/examples/iis-example.ps1 deleted file mode 100644 index 1481aa3f4..000000000 --- a/examples/iis-example.ps1 +++ /dev/null @@ -1,30 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening on port 8085 -Start-PodeServer { - - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http -AllowClientCertificate - New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - Add-PodeTask -Name 'Test' -ScriptBlock { - Start-Sleep -Seconds 10 - 'a message is never late, it arrives exactly when it means to' | Out-Default - } - - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - Write-PodeJsonResponse -Value @{ Message = 'Hello' } - $WebEvent.Request | out-default - } - - Add-PodeRoute -Method Get -Path '/run-task' -ScriptBlock { - Invoke-PodeTask -Name 'Test' | Out-Null - Write-PodeJsonResponse -Value @{ Result = 'jobs done' } - } - -} \ No newline at end of file diff --git a/examples/logging.ps1 b/examples/logging.ps1 deleted file mode 100644 index 65dfe4a02..000000000 --- a/examples/logging.ps1 +++ /dev/null @@ -1,53 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -if (Test-Path -Path "$($path)/src/Pode.psm1" -PathType Leaf) { - Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop -} -else { - Import-Module -Name 'Pode' -} -# or just: -# Import-Module Pode - -$LOGGING_TYPE = 'terminal' # Terminal, File, Custom - -# create a server, and start listening on port 8085 -Start-PodeServer { - - Add-PodeEndpoint -Address localhost -Port 8085 -Protocol Http - Set-PodeViewEngine -Type Pode - - switch ($LOGGING_TYPE.ToLowerInvariant()) { - 'terminal' { - New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging - } - - 'file' { - New-PodeLoggingMethod -File -Name 'requests' -MaxDays 4 | Enable-PodeRequestLogging - } - - 'custom' { - $type = New-PodeLoggingMethod -Custom -ScriptBlock { - param($item) - # send request row to S3 - } - - $type | Enable-PodeRequestLogging - } - } - - # GET request for web page on "localhost:8085/" - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(1, 2, 3); } - } - - # GET request throws fake "500" server error status code - Add-PodeRoute -Method Get -Path '/error' -ScriptBlock { - Set-PodeResponseStatus -Code 500 - } - - # GET request to download a file - Add-PodeRoute -Method Get -Path '/download' -ScriptBlock { - Set-PodeResponseAttachment -Path 'Anger.jpg' - } - -} diff --git a/examples/looping-service.ps1 b/examples/looping-service.ps1 deleted file mode 100644 index 934c965d9..000000000 --- a/examples/looping-service.ps1 +++ /dev/null @@ -1,21 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start looping -Start-PodeServer -Interval 3 { - - Add-PodeHandler -Type Service -Name 'Hello' -ScriptBlock { - Write-Host 'hello, world!' - Lock-PodeObject -ScriptBlock { - "Look I'm locked!" | Out-PodeHost - } - } - - Add-PodeHandler -Type Service -Name 'Bye' -ScriptBlock { - Write-Host 'goodbye!' - } - -} diff --git a/examples/mail-server.ps1 b/examples/mail-server.ps1 deleted file mode 100644 index f08e52b3f..000000000 --- a/examples/mail-server.ps1 +++ /dev/null @@ -1,49 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -<# -Example call: -Send-MailMessage -SmtpServer localhost -To 'to@pode.com' -From 'from@pode.com' -Body 'Hello' -Subject 'Hi there' -Port 25 - -[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { return $true } -Send-MailMessage -SmtpServer localhost -To 'to@pode.com' -From 'from@pode.com' -Body 'Hello' -Subject 'Hi there' -Port 587 -UseSSL -#> - -# create a server, and start listening on port 25 -Start-PodeServer -Threads 2 { - - Add-PodeEndpoint -Address localhost -Protocol Smtp - Add-PodeEndpoint -Address localhost -Protocol Smtps -SelfSigned -TlsMode Explicit - Add-PodeEndpoint -Address localhost -Protocol Smtps -SelfSigned -TlsMode Implicit - - # enable logging - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging -Levels Error, Debug - - # allow the local ip - #Add-PodeAccessRule -Access Allow -Type IP -Values 127.0.0.1 - - # setup an smtp handler - Add-PodeHandler -Type Smtp -Name 'Main' -ScriptBlock { - Write-Host '- - - - - - - - - - - - - - - - - -' - Write-Host $SmtpEvent.Email.From - Write-Host $SmtpEvent.Email.To - Write-Host '|' - Write-Host $SmtpEvent.Email.Body - Write-Host '|' - # Write-Host $SmtpEvent.Email.Data - # Write-Host '|' - $SmtpEvent.Email.Attachments | Out-Default - if ($SmtpEvent.Email.Attachments.Length -gt 0) { - #$SmtpEvent.Email.Attachments[0].Save('C:\temp') - } - Write-Host '|' - $SmtpEvent.Email | Out-Default - $SmtpEvent.Request | out-default - $SmtpEvent.Email.Headers | out-default - Write-Host '- - - - - - - - - - - - - - - - - -' - } - -} \ No newline at end of file diff --git a/examples/modules/external-funcs.psm1 b/examples/modules/External-Funcs.psm1 similarity index 100% rename from examples/modules/external-funcs.psm1 rename to examples/modules/External-Funcs.psm1 diff --git a/examples/modules/Imported-Funcs.ps1 b/examples/modules/Imported-Funcs.ps1 new file mode 100644 index 000000000..6cfb51f44 --- /dev/null +++ b/examples/modules/Imported-Funcs.ps1 @@ -0,0 +1,17 @@ +<# +.SYNOPSIS + Custom function for Web-PageUsing + +.DESCRIPTION + Custom function for Web-PageUsing + +.NOTES + Author: Pode Team + License: MIT License +#> +function Write-MyGreeting { + Write-PodeJsonResponse -Value @{ Message = "Hello, world! [$(Get-Random -Maximum 100)]" } +} + +Export-PodeFunction -Name 'Write-MyGreeting' +Use-PodeScript -Path (Join-Path $PSScriptRoot .\Imported-SubFuncs.ps1) \ No newline at end of file diff --git a/examples/modules/Imported-SubFuncs.ps1 b/examples/modules/Imported-SubFuncs.ps1 new file mode 100644 index 000000000..3db94bb4c --- /dev/null +++ b/examples/modules/Imported-SubFuncs.ps1 @@ -0,0 +1,16 @@ +<# +.SYNOPSIS + Custom function for Imported-Funcs.ps1 + +.DESCRIPTION + Custom function for Imported-Funcs.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +function Write-MySubGreeting { + Write-PodeJsonResponse -Value @{ Message = "Mudkipz! [$(Get-Random -Maximum 100)]" } +} + +Export-PodeFunction -Name 'Write-MySubGreeting' \ No newline at end of file diff --git a/examples/modules/RouteScript.ps1 b/examples/modules/RouteScript.ps1 new file mode 100644 index 000000000..e3a68c6b4 --- /dev/null +++ b/examples/modules/RouteScript.ps1 @@ -0,0 +1,15 @@ +<# +.SYNOPSIS + Custom script for Web-Pages.ps1 and Web-PagesKestrel.ps1 + +.DESCRIPTION + Custom script for Web-Pages.ps1 and Web-PagesKestrel.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +return { + $using:hmm | out-default + Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(4, 5, 6); } +} \ No newline at end of file diff --git a/examples/modules/Script1.ps1 b/examples/modules/Script1.ps1 new file mode 100644 index 000000000..82e22de75 --- /dev/null +++ b/examples/modules/Script1.ps1 @@ -0,0 +1,13 @@ +<# +.SYNOPSIS + Custom script for Dot-SourceScript.ps1 + +.DESCRIPTION + Custom script for Dot-SourceScript.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +Write-Host 'Hello, world!' \ No newline at end of file diff --git a/examples/modules/imported-funcs.ps1 b/examples/modules/imported-funcs.ps1 deleted file mode 100644 index 2602a7f3e..000000000 --- a/examples/modules/imported-funcs.ps1 +++ /dev/null @@ -1,6 +0,0 @@ -function Write-MyGreeting { - Write-PodeJsonResponse -Value @{ Message = "Hello, world! [$(Get-Random -Maximum 100)]" } -} - -Export-PodeFunction -Name 'Write-MyGreeting' -Use-PodeScript -Path (Join-Path $PSScriptRoot .\imported-sub-funcs.ps1) \ No newline at end of file diff --git a/examples/modules/imported-sub-funcs.ps1 b/examples/modules/imported-sub-funcs.ps1 deleted file mode 100644 index c8d533906..000000000 --- a/examples/modules/imported-sub-funcs.ps1 +++ /dev/null @@ -1,5 +0,0 @@ -function Write-MySubGreeting { - Write-PodeJsonResponse -Value @{ Message = "Mudkipz! [$(Get-Random -Maximum 100)]" } -} - -Export-PodeFunction -Name 'Write-MySubGreeting' \ No newline at end of file diff --git a/examples/modules/route_script.ps1 b/examples/modules/route_script.ps1 deleted file mode 100644 index ca7eb39b9..000000000 --- a/examples/modules/route_script.ps1 +++ /dev/null @@ -1,4 +0,0 @@ -return { - $using:hmm | out-default - Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(4, 5, 6); } -} \ No newline at end of file diff --git a/examples/modules/script1.ps1 b/examples/modules/script1.ps1 deleted file mode 100644 index 2935df1f6..000000000 --- a/examples/modules/script1.ps1 +++ /dev/null @@ -1 +0,0 @@ -Write-Host 'Hello, world!' \ No newline at end of file diff --git a/examples/modules/script2.ps1 b/examples/modules/script2.ps1 deleted file mode 100644 index 3a9e22d72..000000000 --- a/examples/modules/script2.ps1 +++ /dev/null @@ -1 +0,0 @@ -Write-Host 'Goodbye, world!' \ No newline at end of file diff --git a/examples/nunit-rest-api.ps1 b/examples/nunit-rest-api.ps1 deleted file mode 100644 index e79155cd0..000000000 --- a/examples/nunit-rest-api.ps1 +++ /dev/null @@ -1,60 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# a typical request to "localhost:8087/api/nunit/run-rest" looks as follows: -<# -{ - "dll": "/path/test.dll", - "tests": [ - "Test.Tests.Method1" - ], - "categories": {} -} -#> - -# create a server, and start listening on port 8087 -Start-PodeServer { - - Add-PodeEndpoint -Address * -Port 8087 -Protocol Http - - # post endpoint, that accepts test to run, and path to test dll - Add-PodeRoute -Method Post -Path '/api/nunit/run-test' -ScriptBlock { - # general - $date = [DateTime]::UtcNow.ToString('yyyy-MM-dd_HH-mm-ss-fffff') - $data = $WebEvent.Data - - # get data passed in - $dll = $data.dll - $tests = $data.tests -join ',' - $catsInclude = $data.categories.include -join ',' - $catsExclude = $data.categories.exclude -join ',' - $results = "$($date).xml" - $outputs = "$($date).txt" - $tool = 'C:\Program Files (x86)\NUnit 2.6.4\bin\nunit-console.exe' - - # run the tests - if (![string]::IsNullOrWhiteSpace($catsInclude)) - { - $catsInclude = "/include=$($catsInclude)" - } - - if (![string]::IsNullOrWhiteSpace($catsExclude)) - { - $catsExclude = "/exclude=$($catsExclude)" - } - - $_args = "/result=$($results) /out=$($outputs) $($catsInclude) $($catsExclude) /run=$($tests) /nologo /nodots `"$($dll)`"" - Start-Process -FilePath $tool -NoNewWindow -Wait -ArgumentList $_args -ErrorAction Stop | Out-Null - - # return results - Write-PodeXmlResponse -Path $results - - # delete results file - Remove-Item -Path $results -Force | Out-Null - Remove-Item -Path $outputs -Force | Out-Null - } - -} \ No newline at end of file diff --git a/examples/one-off-script.ps1 b/examples/one-off-script.ps1 deleted file mode 100644 index f0b192ede..000000000 --- a/examples/one-off-script.ps1 +++ /dev/null @@ -1,13 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# runs the logic once, then exits -Start-PodeServer { - - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - Write-Host 'hello, world!' - -} diff --git a/examples/package.json b/examples/package.json index e8f86a06e..ced2eeb7c 100644 --- a/examples/package.json +++ b/examples/package.json @@ -1,9 +1,9 @@ { "name": "pode", "version": "0.0.0", - "main": "./web-pages.ps1", + "main": "./Web-Pages.ps1", "scripts": { - "start": "./web-pages.ps1", + "start": "./Web-Pages.ps1", "install": "", "test": "", "build": "" diff --git a/examples/public/scripts/simple.js.ps1 b/examples/public/scripts/simple.js.ps1 index 918479b9c..b483c6656 100644 --- a/examples/public/scripts/simple.js.ps1 +++ b/examples/public/scripts/simple.js.ps1 @@ -1,3 +1,14 @@ +<# +.SYNOPSIS + Custom script for index.ps1 + +.DESCRIPTION + Custom script for index.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> return (. { $date = [DateTime]::UtcNow; diff --git a/examples/public/styles/simple.css.ps1 b/examples/public/styles/simple.css.ps1 index 084856f48..a640e3feb 100644 --- a/examples/public/styles/simple.css.ps1 +++ b/examples/public/styles/simple.css.ps1 @@ -1,3 +1,14 @@ +<# +.SYNOPSIS + Custom script for index.ps1 + +.DESCRIPTION + Custom script for index.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> return (. { $date = [DateTime]::UtcNow; diff --git a/examples/relative/dot-source-server.ps1 b/examples/relative/dot-source-server.ps1 index 2b7c26e29..d29df9eb3 100644 --- a/examples/relative/dot-source-server.ps1 +++ b/examples/relative/dot-source-server.ps1 @@ -1,2 +1,14 @@ +<# +.SYNOPSIS + Example of dot-sourcing server logic from a relative path + +.DESCRIPTION + + Example of dot-sourcing server logic from a relative path + +.NOTES + Author: Pode Team + License: MIT License +#> # example of dot-sourcing server logic from a relative path . ../web-pages.ps1 \ No newline at end of file diff --git a/examples/rest-api.ps1 b/examples/rest-api.ps1 deleted file mode 100644 index 0c16c2c53..000000000 --- a/examples/rest-api.ps1 +++ /dev/null @@ -1,35 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening on port 8086 -Start-PodeServer { - - Add-PodeEndpoint -Address 'localhost' -Port 8086 -Protocol Http -DualMode - - # request logging - New-PodeLoggingMethod -Terminal -Batch 10 -BatchTimeout 10 | Enable-PodeRequestLogging - - # can be hit by sending a GET request to "localhost:8086/api/test" - Add-PodeRoute -Method Get -Path '/api/test' -ScriptBlock { - Write-PodeJsonResponse -Value @{ 'hello' = 'world'; } - } - - # can be hit by sending a POST request to "localhost:8086/api/test" - Add-PodeRoute -Method Post -Path '/api/test' -ContentType 'application/json' -ScriptBlock { - Write-PodeJsonResponse -Value @{ 'hello' = 'world'; 'name' = $WebEvent.Data['name']; } - } - - # returns details for an example user - Add-PodeRoute -Method Get -Path '/api/users/:userId' -ScriptBlock { - Write-PodeJsonResponse -Value @{ 'user' = $WebEvent.Parameters['userId']; } - } - - # returns details for an example user - Add-PodeRoute -Method Get -Path '/api/users/:userId/messages' -ScriptBlock { - Write-PodeJsonResponse -Value @{ 'user' = $WebEvent.Parameters['userId']; } - } - -} \ No newline at end of file diff --git a/examples/routes/route.ps1 b/examples/routes/route.ps1 index 131876ce8..1608fb0a6 100644 --- a/examples/routes/route.ps1 +++ b/examples/routes/route.ps1 @@ -1,3 +1,14 @@ +<# +.SYNOPSIS + Used by Web-Pages.ps1 using Use-PodeRoutes + +.DESCRIPTION + Used by Web-Pages.ps1 using Use-PodeRoutes + +.NOTES + Author: Pode Team + License: MIT License +#> Add-PodeRoute -Method Get -Path '/route-file' -ScriptBlock { Write-PodeJsonResponse -Value @{ Message = "I'm from a route file!" } } \ No newline at end of file diff --git a/examples/schedules-cron-helper.ps1 b/examples/schedules-cron-helper.ps1 deleted file mode 100644 index df04b9a4c..000000000 --- a/examples/schedules-cron-helper.ps1 +++ /dev/null @@ -1,20 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -Start-PodeServer { - - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http - - $cron = New-PodeCron -Every Minute -Interval 2 - Add-PodeSchedule -Name 'example' -Cron $cron -ScriptBlock { - 'Hi there!' | Out-Default - } - - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - Write-PodeJsonResponse -Value @{ Result = 1 } - } - -} \ No newline at end of file diff --git a/examples/schedules-long-running.ps1 b/examples/schedules-long-running.ps1 deleted file mode 100644 index 37c0fb8b3..000000000 --- a/examples/schedules-long-running.ps1 +++ /dev/null @@ -1,26 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening on port 8085 -Start-PodeServer { - - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http - - # add lots of schedules that each sleep for a while - 1..30 | ForEach-Object { - Add-PodeSchedule -Name "Schedule_$($_)" -Cron '@minutely' -ArgumentList @{ ID = $_ } -ScriptBlock { - param($ID) - - $seconds = (Get-Random -Minimum 5 -Maximum 40) - Start-Sleep -Seconds $seconds - "ID: $($ID) [$($seconds)]" | Out-PodeHost - } - } - - Set-PodeScheduleConcurrency -Maximum 30 - -} \ No newline at end of file diff --git a/examples/schedules-routes.ps1 b/examples/schedules-routes.ps1 deleted file mode 100644 index cfe3ef0fa..000000000 --- a/examples/schedules-routes.ps1 +++ /dev/null @@ -1,19 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening on port 8085 -Start-PodeServer -EnablePool Schedules { - - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http - - # create a new schdule via a route - Add-PodeRoute -Method Get -Path '/api/schedule' -ScriptBlock { - Add-PodeSchedule -Name 'example' -Cron '@minutely' -ScriptBlock { - 'hello there' | out-default - } - } - -} diff --git a/examples/scripts/routeScript.ps1 b/examples/scripts/routeScript.ps1 index 6ced6153f..6f904d138 100644 --- a/examples/scripts/routeScript.ps1 +++ b/examples/scripts/routeScript.ps1 @@ -1,4 +1,15 @@ +<# +.SYNOPSIS + Used by Create-Routes.ps1 + +.DESCRIPTION + Create-Routes.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> { - $Id = $WebEvent.Parameters['id'] + $Id = $WebEvent.Parameters['id'] Write-PodeJsonResponse -StatusCode 200 -Value @{'id' = $Id } } \ No newline at end of file diff --git a/examples/scripts/schedule.ps1 b/examples/scripts/schedule.ps1 index 82c3fce34..241b34600 100644 --- a/examples/scripts/schedule.ps1 +++ b/examples/scripts/schedule.ps1 @@ -1,3 +1,14 @@ +<# +.SYNOPSIS + Used by Schedules.ps1 + +.DESCRIPTION + Used by Schedules.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> { 'Hello, there!' | Out-PodeHost } \ No newline at end of file diff --git a/examples/scripts/server.ps1 b/examples/scripts/server.ps1 index 5ef8f72d9..c55d4ff6c 100644 --- a/examples/scripts/server.ps1 +++ b/examples/scripts/server.ps1 @@ -1,5 +1,5 @@ { - Add-PodeEndpoint -Address * -Port 8081 -Protocol Http + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging Set-PodeViewEngine -Type Pode diff --git a/examples/server-from-file.ps1 b/examples/server-from-file.ps1 deleted file mode 100644 index 01cdc2353..000000000 --- a/examples/server-from-file.ps1 +++ /dev/null @@ -1,8 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a basic server -Start-PodeServer -FilePath './scripts/server.ps1' -CurrentPath \ No newline at end of file diff --git a/examples/server.psd1 b/examples/server.psd1 index 4d9b598b8..d1858842e 100644 --- a/examples/server.psd1 +++ b/examples/server.psd1 @@ -24,6 +24,9 @@ Compression = @{ Enable = $false } + OpenApi = @{ + UsePodeYamlInternal = $true + } } Server = @{ FileMonitor = @{ diff --git a/examples/sse.ps1 b/examples/sse.ps1 deleted file mode 100644 index 0fa57b0bd..000000000 --- a/examples/sse.ps1 +++ /dev/null @@ -1,35 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening on port 8085 -Start-PodeServer -Threads 3 { - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8090 -Protocol Http - - # log errors - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging -Levels * - - # open local sse connection, and send back data - Add-PodeRoute -Method Get -Path '/data' -ScriptBlock { - ConvertTo-PodeSseConnection -Name 'Data' -Scope Local - Send-PodeSseEvent -Id 1234 -EventType Action -Data 'hello, there!' - Start-Sleep -Seconds 3 - Send-PodeSseEvent -Id 1337 -EventType BoldOne -Data 'general kenobi' - } - - # home page to get sse events - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - Write-PodeViewResponse -Path 'sse-home' - } - - Add-PodeRoute -Method Get -Path '/sse' -ScriptBlock { - ConvertTo-PodeSseConnection -Name 'Test' - } - - Add-PodeTimer -Name 'SendEvent' -Interval 10 -ScriptBlock { - Send-PodeSseEvent -Name 'Test' -Data "An Event! $(Get-Random -Minimum 1 -Maximum 100)" - } -} \ No newline at end of file diff --git a/examples/tasks.ps1 b/examples/tasks.ps1 deleted file mode 100644 index 20caf4bef..000000000 --- a/examples/tasks.ps1 +++ /dev/null @@ -1,42 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a basic server -Start-PodeServer { - - Add-PodeEndpoint -Address * -Port 8081 -Protocol Http - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - Add-PodeTask -Name 'Test1' -ScriptBlock { - 'a string' - 4 - return @{ InnerValue = 'hey look, a value!' } - } - - Add-PodeTask -Name 'Test2' -ScriptBlock { - param($value) - Start-Sleep -Seconds 10 - "a $($value) is never late, it arrives exactly when it means to" | Out-Default - } - - # create a new timer via a route - Add-PodeRoute -Method Get -Path '/api/task/sync' -ScriptBlock { - $result = Invoke-PodeTask -Name 'Test1' -Wait - Write-PodeJsonResponse -Value @{ Result = $result } - } - - Add-PodeRoute -Method Get -Path '/api/task/sync2' -ScriptBlock { - $task = Invoke-PodeTask -Name 'Test1' - $result = ($task | Wait-PodeTask) - Write-PodeJsonResponse -Value @{ Result = $result } - } - - Add-PodeRoute -Method Get -Path '/api/task/async' -ScriptBlock { - Invoke-PodeTask -Name 'Test2' -ArgumentList @{ value = 'wizard' } | Out-Null - Write-PodeJsonResponse -Value @{ Result = 'jobs done' } - } - -} diff --git a/examples/tcp-server-auth.ps1 b/examples/tcp-server-auth.ps1 deleted file mode 100644 index eaf75559c..000000000 --- a/examples/tcp-server-auth.ps1 +++ /dev/null @@ -1,37 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening on port 9000 -Start-PodeServer -Threads 2 { - - # add endpoint - Add-PodeEndpoint -Address * -Port 9000 -Protocol Tcp -CRLFMessageEnd - - # enable logging - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - # create a role access method get retrieves roles from a database - New-PodeAccessScheme -Type Role | Add-PodeAccess -Name 'RoleExample' -ScriptBlock { - param($username) - if ($username -ieq 'morty') { - return @('Developer') - } - - return 'QA' - } - - # setup a Verb that only allows Developers - Add-PodeVerb -Verb 'EXAMPLE :username' -ScriptBlock { - if (!(Test-PodeAccess -Name 'RoleExample' -Destination 'Developer' -ArgumentList $TcpEvent.Parameters.username)) { - Write-PodeTcpClient -Message 'Forbidden Access' - 'Forbidden!' | Out-Default - return - } - - Write-PodeTcpClient -Message 'Hello, there!' - 'Hello!' | Out-Default - } -} \ No newline at end of file diff --git a/examples/tcp-server-http.ps1 b/examples/tcp-server-http.ps1 deleted file mode 100644 index 8d1f07b5e..000000000 --- a/examples/tcp-server-http.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening on port 9000 -Start-PodeServer -Threads 2 { - - # add two endpoints - Add-PodeEndpoint -Address * -Port 9000 -Protocol Tcp - - # enable logging - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - # catch-all for http - Add-PodeVerb -Verb '*' -Close -ScriptBlock { - $TcpEvent.Request.Body | Out-Default - Write-PodeTcpClient -Message "HTTP/1.1 200 `r`nConnection: close`r`n`r`nHello, there" - # navigate to "http://localhost:9000" - } - -} \ No newline at end of file diff --git a/examples/tcp-server-multi-endpoint.ps1 b/examples/tcp-server-multi-endpoint.ps1 deleted file mode 100644 index 85f7088b5..000000000 --- a/examples/tcp-server-multi-endpoint.ps1 +++ /dev/null @@ -1,37 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening on port 9000 -Start-PodeServer -Threads 2 { - - # add two endpoints - Add-PodeEndpoint -Address * -Port 9000 -Protocol Tcp -Name 'EP1' -Acknowledge 'Hello there!' -CRLFMessageEnd - Add-PodeEndpoint -Address '127.0.0.2' -Hostname 'foo.pode.com' -Port 9000 -Protocol Tcp -Name 'EP2' -Acknowledge 'Hello there!' -CRLFMessageEnd - - # enable logging - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - # hello verb for endpoint1 - Add-PodeVerb -Verb 'HELLO :forename :surname' -EndpointName EP1 -ScriptBlock { - Write-PodeTcpClient -Message "HI 1, $($TcpEvent.Parameters.forename) $($TcpEvent.Parameters.surname)" - "HI 1, $($TcpEvent.Parameters.forename) $($TcpEvent.Parameters.surname)" | Out-Default - } - - # hello verb for endpoint2 - Add-PodeVerb -Verb 'HELLO :forename :surname' -EndpointName EP2 -ScriptBlock { - Write-PodeTcpClient -Message "HI 2, $($TcpEvent.Parameters.forename) $($TcpEvent.Parameters.surname)" - "HI 2, $($TcpEvent.Parameters.forename) $($TcpEvent.Parameters.surname)" | Out-Default - } - - # catch-all verb for both endpoints - Add-PodeVerb -Verb '*' -ScriptBlock { - Write-PodeTcpClient -Message "Unrecognised verb sent" - } - - # quit verb for both endpoints - Add-PodeVerb -Verb 'Quit' -Close - -} \ No newline at end of file diff --git a/examples/tcp-server.ps1 b/examples/tcp-server.ps1 deleted file mode 100644 index c28677b46..000000000 --- a/examples/tcp-server.ps1 +++ /dev/null @@ -1,55 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening on port 9000 -Start-PodeServer -Threads 2 { - - # add two endpoints - Add-PodeEndpoint -Address * -Port 9000 -Protocol Tcp -CRLFMessageEnd #-Acknowledge 'Welcome!' - # Add-PodeEndpoint -Address * -Port 9000 -Protocol Tcps -SelfSigned -CRLFMessageEnd -TlsMode Explicit -Acknowledge 'Welcome!' - # Add-PodeEndpoint -Address * -Port 9000 -Protocol Tcps -SelfSigned -CRLFMessageEnd -TlsMode Implicit -Acknowledge 'Welcome!' - - # enable logging - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - Add-PodeVerb -Verb 'HELLO' -ScriptBlock { - Write-PodeTcpClient -Message "HI" - 'here' | Out-Default - } - - Add-PodeVerb -Verb 'HELLO2 :username' -ScriptBlock { - Write-PodeTcpClient -Message "HI2, $($TcpEvent.Parameters.username)" - } - - Add-PodeVerb -Verb * -ScriptBlock { - Write-PodeTcpClient -Message 'Unrecognised verb sent' - } - - # Add-PodeVerb -Verb * -Close -ScriptBlock { - # $TcpEvent.Request.Body | Out-Default - # Write-PodeTcpClient -Message "HTTP/1.1 200 `r`nConnection: close`r`n`r`nHello, there" - # } - - # Add-PodeVerb -Verb 'STARTTLS' -UpgradeToSsl - - # Add-PodeVerb -Verb 'STARTTLS' -ScriptBlock { - # Write-PodeTcpClient -Message 'TLS GO AHEAD' - # $TcpEvent.Request.UpgradeToSSL() - # } - - # Add-PodeVerb -Verb 'QUIT' -Close - - Add-PodeVerb -Verb 'QUIT' -ScriptBlock { - Write-PodeTcpClient -Message 'Bye!' - Close-PodeTcpClient - } - - Add-PodeVerb -Verb 'HELLO3' -ScriptBlock { - Write-PodeTcpClient -Message "Hi! What's your name?" - $name = Read-PodeTcpClient -CRLFMessageEnd - Write-PodeTcpClient -Message "Hi, $($name)!" - } -} \ No newline at end of file diff --git a/examples/timers-route.ps1 b/examples/timers-route.ps1 deleted file mode 100644 index 8e62d55ce..000000000 --- a/examples/timers-route.ps1 +++ /dev/null @@ -1,19 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a basic server -Start-PodeServer -EnablePool Timers { - - Add-PodeEndpoint -Address * -Port 8081 -Protocol Http - - # create a new timer via a route - Add-PodeRoute -Method Get -Path '/api/timer' -ScriptBlock { - Add-PodeTimer -Name 'example' -Interval 5 -ScriptBlock { - 'hello there' | out-default - } - } - -} diff --git a/examples/web-RestOpenApiFuncs.ps1 b/examples/web-RestOpenApiFuncs.ps1 new file mode 100644 index 000000000..50a60b648 --- /dev/null +++ b/examples/web-RestOpenApiFuncs.ps1 @@ -0,0 +1,55 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with OpenAPI integration and various OpenAPI viewers. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081 with OpenAPI documentation. + It demonstrates how to use OpenAPI for documenting APIs and provides various OpenAPI viewers such as Swagger, ReDoc, RapiDoc, StopLight, Explorer, and RapiPdf. + +.EXAMPLE + To run the sample: ./Web-RestOpenApi.ps1 + + OpenAPI Info: + Specification: + http://localhost:8081/openapi + Documentation: + http://localhost:8081/docs + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-RestOpenApi.ps1 +.NOTES + Author: Pode Team + License: MIT License +#> +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +Start-PodeServer { + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + Enable-PodeOpenApi -DisableMinimalDefinitions + Add-PodeOAInfo -Title 'OpenAPI Example' + Enable-PodeOpenApiViewer -Type Swagger -Path '/docs/swagger' + Enable-PodeOpenApiViewer -Type ReDoc -Path '/docs/redoc' + Enable-PodeOpenApiViewer -Type RapiDoc -Path '/docs/rapidoc' + Enable-PodeOpenApiViewer -Type StopLight -Path '/docs/stoplight' + Enable-PodeOpenApiViewer -Type Explorer -Path '/docs/explorer' + Enable-PodeOpenApiViewer -Type RapiPdf -Path '/docs/rapipdf' + + Enable-PodeOpenApiViewer -Bookmarks -Path '/docs' + + #ConvertTo-PodeRoute -Path '/api' -Commands @('Get-ChildItem', 'New-Item') + ConvertTo-PodeRoute -Path '/api' -Module Pester +} \ No newline at end of file diff --git a/examples/web-auth-apikey-jwt.ps1 b/examples/web-auth-apikey-jwt.ps1 deleted file mode 100644 index 0b94b8dcd..000000000 --- a/examples/web-auth-apikey-jwt.ps1 +++ /dev/null @@ -1,69 +0,0 @@ -param( - [Parameter()] - [ValidateSet('Header', 'Query', 'Cookie')] - [string] - $Location = 'Header' -) - -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# ------------- -# None Signed -# Req: Invoke-RestMethod -Uri 'http://localhost:8085/users' -Headers @{ 'X-API-KEY' = 'eyJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6Im1vcnR5Iiwic3ViIjoiMTIzIn0.' } -# ------------- - -# ------------- -# Signed -# Req: Invoke-RestMethod -Uri 'http://localhost:8085/users' -Headers @{ 'X-API-KEY' = 'eyJhbGciOiJoczI1NiJ9.eyJ1c2VybmFtZSI6Im1vcnR5Iiwic3ViIjoiMTIzIn0.WIOvdwk4mNrNC9EtTcQccmLHJc02gAuonXClHMFOjKM' } -# -# (add -Secret 'secret' to New-PodeAuthScheme below) -# ------------- - -# create a server, and start listening on port 8085 -Start-PodeServer -Threads 2 { - - # listen on localhost:8085 - Add-PodeEndpoint -Address localhost -Port 8085 -Protocol Http - - New-PodeLoggingMethod -File -Name 'requests' | Enable-PodeRequestLogging - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - # setup bearer auth - New-PodeAuthScheme -ApiKey -Location $Location -AsJWT | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { - param($jwt) - - # here you'd check a real user storage, this is just for example - if ($jwt.username -ieq 'morty') { - return @{ - User = @{ - ID ='M0R7Y302' - Name = 'Morty' - Type = 'Human' - } - } - } - - return $null - } - - # GET request to get list of users (since there's no session, authentication will always happen) - Add-PodeRoute -Method Get -Path '/users' -Authentication 'Validate' -ScriptBlock { - Write-PodeJsonResponse -Value @{ - Users = @( - @{ - Name = 'Deep Thought' - Age = 42 - }, - @{ - Name = 'Leeroy Jenkins' - Age = 1337 - } - ) - } - } - -} \ No newline at end of file diff --git a/examples/web-auth-apikey.ps1 b/examples/web-auth-apikey.ps1 deleted file mode 100644 index 580b6c4cc..000000000 --- a/examples/web-auth-apikey.ps1 +++ /dev/null @@ -1,59 +0,0 @@ -param( - [Parameter()] - [ValidateSet('Header', 'Query', 'Cookie')] - [string] - $Location = 'Header' -) - -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# Invoke-RestMethod -Method Get -Uri 'http://localhost:8085/users' -Headers @{ 'X-API-KEY' = 'test-api-key' } - -# create a server, and start listening on port 8085 -Start-PodeServer -Threads 2 { - - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http - - New-PodeLoggingMethod -File -Name 'requests' | Enable-PodeRequestLogging - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - # setup bearer auth - New-PodeAuthScheme -ApiKey -Location $Location | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { - param($key) - - # here you'd check a real user storage, this is just for example - if ($key -ieq 'test-api-key') { - return @{ - User = @{ - ID ='M0R7Y302' - Name = 'Morty' - Type = 'Human' - } - } - } - - return $null - } - - # GET request to get list of users (since there's no session, authentication will always happen) - Add-PodeRoute -Method Get -Path '/users' -Authentication 'Validate' -ScriptBlock { - Write-PodeJsonResponse -Value @{ - Users = @( - @{ - Name = 'Deep Thought' - Age = 42 - }, - @{ - Name = 'Leeroy Jenkins' - Age = 1337 - } - ) - } - } - -} \ No newline at end of file diff --git a/examples/web-auth-basic-adhoc.ps1 b/examples/web-auth-basic-adhoc.ps1 deleted file mode 100644 index ebcb9bb7e..000000000 --- a/examples/web-auth-basic-adhoc.ps1 +++ /dev/null @@ -1,67 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -<# -This example shows how to use sessionless authentication, which will mostly be for -REST APIs. The example used here is adhoc Basic authentication. - -Calling the '[POST] http://localhost:8085/users' endpoint, with an Authorization -header of 'Basic bW9ydHk6cGlja2xl' will display the users. Anything else and -you'll get a 401 status code back. - -Success: -Invoke-RestMethod -Uri http://localhost:8085/users -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' } - -Failure: -Invoke-RestMethod -Uri http://localhost:8085/users -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cmljaw==' } -#> - -# create a server, and start listening on port 8085 -Start-PodeServer -Threads 2 { - - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http - - # setup basic auth (base64> username:password in header) - New-PodeAuthScheme -Basic -Realm 'Pode Example Page' | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { - param($username, $password) - - # here you'd check a real user storage, this is just for example - if ($username -eq 'morty' -and $password -eq 'pickle') { - return @{ - User = @{ - ID ='M0R7Y302' - Name = 'Morty' - Type = 'Human' - } - } - } - - return @{ Message = 'Invalid details supplied' } - } - - # POST request to get list of users (authentication is done adhoc, and not directly using -Authentication on the Route) - Add-PodeRoute -Method Post -Path '/users' -ScriptBlock { - if (!(Test-PodeAuth -Name Validate)) { - Set-PodeResponseStatus -Code 401 - return - } - - Write-PodeJsonResponse -Value @{ - User = @( - @{ - Name = 'Deep Thought' - Age = 42 - }, - @{ - Name = 'Leeroy Jenkins' - Age = 1337 - } - ) - } - } - -} \ No newline at end of file diff --git a/examples/web-auth-basic-anon.ps1 b/examples/web-auth-basic-anon.ps1 deleted file mode 100644 index 024e50d3b..000000000 --- a/examples/web-auth-basic-anon.ps1 +++ /dev/null @@ -1,74 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -<# -This example shows how to use sessionless authentication, which will mostly be for -REST APIs. The example used here is Basic authentication. - -Calling the '[POST] http://localhost:8085/users' endpoint, with an Authorization -header of 'Basic bW9ydHk6cGlja2xl' will display the uesrs. Anything else and -you'll get a 401 status code back. - -Success: -Invoke-RestMethod -Uri http://localhost:8085/users -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' } - -Failure: -Invoke-RestMethod -Uri http://localhost:8085/users -Headers @{ Authorization = 'Basic bW9ydHk6cmljaw==' } -#> - -# create a server, and start listening on port 8085 -Start-PodeServer -Threads 2 { - - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http - - # setup basic auth (base64> username:password in header) - New-PodeAuthScheme -Basic -Realm 'Pode Example Page' | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { - param($username, $password) - - # here you'd check a real user storage, this is just for example - if ($username -eq 'morty' -and $password -eq 'pickle') { - return @{ - User = @{ - ID ='M0R7Y302' - Name = 'Morty' - Type = 'Human' - } - } - } - - return @{ Message = 'Invalid details supplied' } - } - - # GET request to get list of users (since there's no session, authentication will always happen, but, we're allowing anon access) - Add-PodeRoute -Method Get -Path '/users' -Authentication 'Validate' -AllowAnon -ScriptBlock { - if (Test-PodeAuthUser) { - Write-PodeJsonResponse -Value @{ - Users = @( - @{ - Name = 'Deep Thought' - Age = 42 - }, - @{ - Name = 'Leeroy Jenkins' - Age = 1337 - } - ) - } - } - else { - Write-PodeJsonResponse -Value @{ - Users = @( - @{ - Name = 'John Smith' - Age = 21 - } - ) - } - } - } - -} \ No newline at end of file diff --git a/examples/web-auth-basic-header.ps1 b/examples/web-auth-basic-header.ps1 deleted file mode 100644 index 8f11208d9..000000000 --- a/examples/web-auth-basic-header.ps1 +++ /dev/null @@ -1,73 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -<# -This example shows how to use session authentication on REST APIs using Headers. -The example used here is Basic authentication. - -Login: -$session = (Invoke-WebRequest -Uri http://localhost:8085/login -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' }).Headers['pode.sid'] - -Users: -Invoke-RestMethod -Uri http://localhost:8085/users -Method Post -Headers @{ 'pode.sid' = "$session" } - -Logout: -Invoke-WebRequest -Uri http://localhost:8085/logout -Method Post -Headers @{ 'pode.sid' = "$session" } -#> - -# create a server, and start listening on port 8085 -Start-PodeServer -Threads 2 { - - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http - - # enable error logging - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - # setup session details - Enable-PodeSessionMiddleware -Duration 120 -Extend -UseHeaders -Strict - - # setup basic auth (base64> username:password in header) - New-PodeAuthScheme -Basic | Add-PodeAuth -Name 'Login' -ScriptBlock { - param($username, $password) - - # here you'd check a real user storage, this is just for example - if ($username -eq 'morty' -and $password -eq 'pickle') { - return @{ - User = @{ - ID ='M0R7Y302' - Name = 'Morty' - Type = 'Human' - } - } - } - - return @{ Message = 'Invalid details supplied' } - } - - # POST request to login - Add-PodeRoute -Method Post -Path '/login' -Authentication 'Login' - - # POST request to logout - Add-PodeRoute -Method Post -Path '/logout' -Authentication 'Login' -Logout - - # POST request to get list of users - the "pode.sid" header is expected - Add-PodeRoute -Method Post -Path '/users' -Authentication 'Login' -ScriptBlock { - Write-PodeJsonResponse -Value @{ - Users = @( - @{ - Name = 'Deep Thought' - Age = 42 - }, - @{ - Name = 'Leeroy Jenkins' - Age = 1337 - } - ) - } - } - -} \ No newline at end of file diff --git a/examples/web-auth-basic.ps1 b/examples/web-auth-basic.ps1 deleted file mode 100644 index 1ac920dd4..000000000 --- a/examples/web-auth-basic.ps1 +++ /dev/null @@ -1,62 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -if (Test-Path -Path "$($path)/src/Pode.psm1" -PathType Leaf) { - Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop -} -else { - Import-Module -Name 'Pode' -} - -# or just: -# Import-Module Pode - -<# -This example shows how to use sessionless authentication, which will mostly be for -REST APIs. The example used here is Basic authentication. - -Calling the '[POST] http://localhost:8085/users' endpoint, with an Authorization -header of 'Basic bW9ydHk6cGlja2xl' will display the uesrs. Anything else and -you'll get a 401 status code back. - -Success: -Invoke-RestMethod -Uri http://localhost:8085/users -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' } - -Failure: -Invoke-RestMethod -Uri http://localhost:8085/users -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cmljaw==' } -#> - -# create a server, and start listening on port 8085 -Start-PodeServer -Threads 2 { - - # listen on localhost:8085 - Add-PodeEndpoint -Address localhost -Port 8085 -Protocol Http - - # request logging - New-PodeLoggingMethod -Terminal -Batch 10 -BatchTimeout 10 | Enable-PodeRequestLogging - - # setup basic auth (base64> username:password in header) - New-PodeAuthScheme -Basic -Realm 'Pode Example Page' | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { - param($username, $password) - - # here you'd check a real user storage, this is just for example - if ($username -eq 'morty' -and $password -eq 'pickle') { - return @{ - User = @{ - Username = 'morty' - ID ='M0R7Y302' - Name = 'Morty' - Type = 'Human' - } - } - } - - return @{ Message = 'Invalid details supplied' } - } - - # POST request to get current user (since there's no session, authentication will always happen) - Add-PodeRoute -Method Post -Path '/users' -Authentication 'Validate' -ScriptBlock { - Write-PodeJsonResponse -Value @{ - User = (Get-PodeAuthUser) - } - } - -} \ No newline at end of file diff --git a/examples/web-auth-bearer.ps1 b/examples/web-auth-bearer.ps1 deleted file mode 100644 index 3e8ca4e74..000000000 --- a/examples/web-auth-bearer.ps1 +++ /dev/null @@ -1,52 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# Invoke-RestMethod -Method Get -Uri 'http://localhost:8085/users' -Headers @{ Authorization = 'Bearer test-token' } - -# create a server, and start listening on port 8085 -Start-PodeServer -Threads 2 { - - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http - - New-PodeLoggingMethod -File -Name 'requests' | Enable-PodeRequestLogging - - # setup bearer auth - New-PodeAuthScheme -Bearer -Scope write | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { - param($token) - - # here you'd check a real user storage, this is just for example - if ($token -ieq 'test-token') { - return @{ - User = @{ - ID ='M0R7Y302' - Name = 'Morty' - Type = 'Human' - } - Scope = 'write' - } - } - - return $null - } - - # GET request to get list of users (since there's no session, authentication will always happen) - Add-PodeRoute -Method Get -Path '/users' -Authentication 'Validate' -ScriptBlock { - Write-PodeJsonResponse -Value @{ - Users = @( - @{ - Name = 'Deep Thought' - Age = 42 - }, - @{ - Name = 'Leeroy Jenkins' - Age = 1337 - } - ) - } - } - -} \ No newline at end of file diff --git a/examples/web-auth-digest.ps1 b/examples/web-auth-digest.ps1 deleted file mode 100644 index 4629a09dd..000000000 --- a/examples/web-auth-digest.ps1 +++ /dev/null @@ -1,48 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening on port 8085 -Start-PodeServer -Threads 2 { - - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http - - # setup digest auth - New-PodeAuthScheme -Digest | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { - param($username, $params) - - # here you'd check a real user storage, this is just for example - if ($username -ieq 'morty') { - return @{ - User = @{ - ID ='M0R7Y302' - Name = 'Morty' - Type = 'Human' - } - Password = 'pickle' - } - } - - return $null - } - - # GET request to get list of users (since there's no session, authentication will always happen) - Add-PodeRoute -Method Get -Path '/users' -Authentication 'Validate' -ScriptBlock { - Write-PodeJsonResponse -Value @{ - Users = @( - @{ - Name = 'Deep Thought' - Age = 42 - }, - @{ - Name = 'Leeroy Jenkins' - Age = 1337 - } - ) - } - } - -} \ No newline at end of file diff --git a/examples/web-auth-form-file.ps1 b/examples/web-auth-form-file.ps1 deleted file mode 100644 index 207a4ca98..000000000 --- a/examples/web-auth-form-file.ps1 +++ /dev/null @@ -1,68 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -<# -This examples shows how to use session persistant authentication using a user file. -The example used here is Form authentication, sent from the in HTML. - -Navigating to the 'http://localhost:8085' endpoint in your browser will auto-rediect you to the '/login' -page. Here, you can type the details for a user in the json file. Clicking 'Login' will take you back to the home -page with a greeting and a view counter. Clicking 'Logout' will purge the session and take you back to -the login page. - -username = r.sanchez -password = pickle -#> - -# create a server, and start listening on port 8085 -Start-PodeServer -Threads 2 { - - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - # set the view engine - Set-PodeViewEngine -Type Pode - - # setup session details - Enable-PodeSessionMiddleware -Duration 120 -Extend - - # setup form auth against user file ( in HTML) - New-PodeAuthScheme -Form | Add-PodeAuthUserFile -Name 'Login' -FilePath './users/users.json' -FailureUrl '/login' -SuccessUrl '/' - - - # home page: - # redirects to login page if not authenticated - Add-PodeRoute -Method Get -Path '/' -Authentication Login -ScriptBlock { - $WebEvent.Session.Data.Views++ - - Write-PodeViewResponse -Path 'auth-home' -Data @{ - Username = $WebEvent.Auth.User.Name - Views = $WebEvent.Session.Data.Views - } - } - - - # login page: - # the login flag set below checks if there is already an authenticated session cookie. If there is, then - # the user is redirected to the home page. If there is no session then the login page will load without - # checking user authetication (to prevent a 401 status) - Add-PodeRoute -Method Get -Path '/login' -Authentication Login -Login -ScriptBlock { - Write-PodeViewResponse -Path 'auth-login' -FlashMessages - } - - - # login check: - # this is the endpoint the 's action will invoke. If the user validates then they are set against - # the session as authenticated, and redirect to the home page. If they fail, then the login page reloads - Add-PodeRoute -Method Post -Path '/login' -Authentication Login -Login - - - # logout check: - # when the logout button is click, this endpoint is invoked. The logout flag set below informs this call - # to purge the currently authenticated session, and then redirect back to the login page - Add-PodeRoute -Method Post -Path '/logout' -Authentication Login -Logout -} \ No newline at end of file diff --git a/examples/web-auth-oauth2.ps1 b/examples/web-auth-oauth2.ps1 deleted file mode 100644 index 4faad0570..000000000 --- a/examples/web-auth-oauth2.ps1 +++ /dev/null @@ -1,62 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -<# -This examples shows how to use session persistant authentication using Azure AD and OAuth2 - -Navigating to the 'http://localhost:8085' endpoint in your browser will auto-rediect you to Azure. -There, login to Azure and you'll be redirected back to the home page - -Note: You'll need to register a new app in Azure, and note you clientId, secret, and tenant - in the variables below. -#> - -# create a server, and start listening on port 8085 -Start-PodeServer -Threads 2 { - - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http -Default - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - # set the view engine - Set-PodeViewEngine -Type Pode - - # setup session details - Enable-PodeSessionMiddleware -Duration 120 -Extend - - # setup auth against Azure AD (the following are from registering an app in the portal) - $clientId = '' - $clientSecret = '' - $tenantId = '' - - $scheme = New-PodeAuthAzureADScheme -Tenant $tenantId -ClientId $clientId -ClientSecret $clientSecret - $scheme | Add-PodeAuth -Name 'Login' -FailureUrl '/login' -SuccessUrl '/' -ScriptBlock { - param($user, $accessToken, $refreshToken) - return @{ User = $user } - } - - - # home page: - # redirects to login page if not authenticated - Add-PodeRoute -Method Get -Path '/' -Authentication Login -ScriptBlock { - $WebEvent.Session.Data.Views++ - - Write-PodeViewResponse -Path 'auth-home' -Data @{ - Username = $WebEvent.Auth.User.name - Views = $WebEvent.Session.Data.Views - } - } - - - # login - this will just redirect to azure - Add-PodeRoute -Method Get -Path '/login' -Authentication Login - - - # logout check: - # when the logout button is click, this endpoint is invoked. The logout flag set below informs this call - # to purge the currently authenticated session, and then redirect back to the login page - Add-PodeRoute -Method Post -Path '/logout' -Authentication Login -Logout -} \ No newline at end of file diff --git a/examples/web-csrf.ps1 b/examples/web-csrf.ps1 deleted file mode 100644 index e6c8b78f9..000000000 --- a/examples/web-csrf.ps1 +++ /dev/null @@ -1,48 +0,0 @@ -param( - [Parameter()] - [ValidateSet('Cookie', 'Session')] - [string] - $Type = 'Session' -) - -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening on port 8090 -Start-PodeServer -Threads 2 { - - # listen on localhost:8090 - Add-PodeEndpoint -Address localhost -Port 8090 -Protocol Http - - # set view engine to pode renderer - Set-PodeViewEngine -Type Pode - - # set csrf middleware, then either session middleware, or cookie global secret - switch ($Type.ToLowerInvariant()) { - 'cookie' { - Set-PodeCookieSecret -Value 'rem' -Global - Enable-PodeCsrfMiddleware -UseCookies - } - - 'session' { - Enable-PodeSessionMiddleware -Duration 120 - Enable-PodeCsrfMiddleware - } - } - - # GET request for index page, and to make a token - # this route will work, as GET methods are ignored by CSRF by default - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - $token = (New-PodeCsrfToken) - Write-PodeViewResponse -Path 'index-csrf' -Data @{ 'csrfToken' = $token } -FlashMessages - } - - # POST route for form with and without csrf token - Add-PodeRoute -Method Post -Path '/token' -ScriptBlock { - Move-PodeResponseUrl -Url '/' - } - -} \ No newline at end of file diff --git a/examples/web-funcs-to-routes.ps1 b/examples/web-funcs-to-routes.ps1 deleted file mode 100644 index dc21f4784..000000000 --- a/examples/web-funcs-to-routes.ps1 +++ /dev/null @@ -1,35 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# create a server, and start listening on port 8085 -Start-PodeServer -Threads 2 { - - # setup basic auth (base64> username:password in header) - New-PodeAuthScheme -Basic | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { - param($username, $password) - - # here you'd check a real user storage, this is just for example - if ($username -eq 'morty' -and $password -eq 'pickle') { - return @{ - User = @{ - ID ='M0R7Y302' - Name = 'Morty' - Type = 'Human' - } - } - } - - return @{ Message = 'Invalid details supplied' } - } - - # listen on localhost:8090 - Add-PodeEndpoint -Address localhost -Port 8090 -Protocol Http - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - # make routes for functions - with every route requires authentication - ConvertTo-PodeRoute -Commands @('Get-ChildItem', 'Get-Host', 'Invoke-Expression') -Authentication Validate -Verbose - - # make routes for every exported command in Pester - # ConvertTo-PodeRoute -Module Pester -Verbose - -} diff --git a/examples/web-gui.ps1 b/examples/web-gui.ps1 deleted file mode 100644 index fec2aeb88..000000000 --- a/examples/web-gui.ps1 +++ /dev/null @@ -1,25 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening on port 8090 -Start-PodeServer { - - # listen on localhost:8090 - Add-PodeEndpoint -Address localhost -Port 8090 -Protocol Http -Name 'local1' - Add-PodeEndpoint -Address localhost -Port 8091 -Protocol Http -Name 'local2' - - # tell this server to run as a desktop gui - Show-PodeGui -Title 'Pode Desktop Application' -Icon '../images/icon.png' -EndpointName 'local2' -ResizeMode 'NoResize' - - # set view engine to pode renderer - Set-PodeViewEngine -Type Pode - - # GET request for web page on "localhost:8090/" - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - Write-PodeViewResponse -Path 'gui' -Data @{ 'numbers' = @(1, 2, 3); } - } - - } \ No newline at end of file diff --git a/examples/web-gzip-request.ps1 b/examples/web-gzip-request.ps1 deleted file mode 100644 index a3d1db2bc..000000000 --- a/examples/web-gzip-request.ps1 +++ /dev/null @@ -1,20 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening on port 8085 -Start-PodeServer -Threads 2 { - - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http - - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - # GET request that receives gzip'd json - Add-PodeRoute -Method Post -Path '/users' -ScriptBlock { - Write-PodeJsonResponse -Value $WebEvent.Data - } - -} \ No newline at end of file diff --git a/examples/web-hostname-kestrel.ps1 b/examples/web-hostname-kestrel.ps1 deleted file mode 100644 index 0003a355d..000000000 --- a/examples/web-hostname-kestrel.ps1 +++ /dev/null @@ -1,44 +0,0 @@ -param( - [int] - $Port = 8085 -) - -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# you will require the Pode.Kestrel module for this example -Import-Module Pode.Kestrel - -# create a server, and start listening on port 8085 at pode.foo.com -# -- You will need to add "127.0.0.1 pode.foo.com" to your hosts file -Start-PodeServer -Threads 2 -ListenerType Kestrel { - - # listen on localhost:8085 - Add-PodeEndpoint -Address pode3.foo.com -Port $Port -Protocol Http - Add-PodeEndpoint -Address pode2.foo.com -Port $Port -Protocol Http - Add-PodeEndpoint -Address 127.0.0.1 -Hostname pode.foo.com -Port $Port -Protocol Http - Add-PodeEndpoint -Hostname pode4.foo.com -Port $Port -Protocol Http -LookupHostname - - # logging - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - # set view engine to pode renderer - Set-PodeViewEngine -Type Pode - - # STATIC asset folder route - Add-PodeStaticRoute -Path '/assets' -Source './assets' -Defaults @('index.html') - - # GET request for web page on "localhost:8085/" - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - Write-PodeViewResponse -Path 'web-static' -Data @{ 'numbers' = @(1, 2, 3); } - } - - # GET request to download a file from static route - Add-PodeRoute -Method Get -Path '/download' -ScriptBlock { - Set-PodeResponseAttachment -Path '/assets/images/Fry.png' - } - -} \ No newline at end of file diff --git a/examples/web-hostname.ps1 b/examples/web-hostname.ps1 deleted file mode 100644 index 920dfd2d5..000000000 --- a/examples/web-hostname.ps1 +++ /dev/null @@ -1,41 +0,0 @@ -param( - [int] - $Port = 8085 -) - -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening on port 8085 at pode.foo.com -# -- You will need to add "127.0.0.1 pode.foo.com" to your hosts file -Start-PodeServer -Threads 2 { - - # listen on localhost:8085 - Add-PodeEndpoint -Address pode3.foo.com -Port $Port -Protocol Http - Add-PodeEndpoint -Address pode2.foo.com -Port $Port -Protocol Http - Add-PodeEndpoint -Address 127.0.0.1 -Hostname pode.foo.com -Port $Port -Protocol Http - Add-PodeEndpoint -Hostname pode4.foo.com -Port $Port -Protocol Http -LookupHostname - - # logging - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - # set view engine to pode renderer - Set-PodeViewEngine -Type Pode - - # STATIC asset folder route - Add-PodeStaticRoute -Path '/assets' -Source './assets' -Defaults @('index.html') - - # GET request for web page on "localhost:8085/" - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - Write-PodeViewResponse -Path 'web-static' -Data @{ 'numbers' = @(1, 2, 3); } - } - - # GET request to download a file from static route - Add-PodeRoute -Method Get -Path '/download' -ScriptBlock { - Set-PodeResponseAttachment -Path '/assets/images/Fry.png' - } - -} \ No newline at end of file diff --git a/examples/web-imports.ps1 b/examples/web-imports.ps1 deleted file mode 100644 index e6017a3cc..000000000 --- a/examples/web-imports.ps1 +++ /dev/null @@ -1,30 +0,0 @@ -param( - [int] - $Port = 8085 -) - -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# import modules -Import-Module -Name EPS - -# create a server, and start listening on port 8085 -Start-PodeServer -Threads 2 { - - # listen on localhost:8085 - Add-PodeEndpoint -Address localhost -Port $Port -Protocol Http - - # set view engine to pode renderer - Set-PodeViewEngine -Type Pode - - # GET request for web page on "localhost:8085/" - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - Get-Module | Out-Default - Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(1, 2, 3); } - } - -} \ No newline at end of file diff --git a/examples/web-metrics.ps1 b/examples/web-metrics.ps1 deleted file mode 100644 index d0d829751..000000000 --- a/examples/web-metrics.ps1 +++ /dev/null @@ -1,18 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -Start-PodeServer -Threads 2 { - - Add-PodeEndpoint -Address * -Port 8080 -Protocol Http - - Add-PodeRoute -Method Get -Path '/uptime' -ScriptBlock { - Write-PodeJsonResponse -Value @{ - Restarts = (Get-PodeServerRestartCount) - Uptime = @{ - Session = (Get-PodeServerUptime) - Total = (Get-PodeServerUptime -Total) - } - } - } - -} \ No newline at end of file diff --git a/examples/web-pages-https.ps1 b/examples/web-pages-https.ps1 deleted file mode 100644 index a62a40c28..000000000 --- a/examples/web-pages-https.ps1 +++ /dev/null @@ -1,34 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# web-pages-https.ps1 example notes: -# ----------------------------------# -# to use the hostname listener, you'll need to add "pode.foo.com 127.0.0.1" to your hosts file -# ---------------------------------- - -# create a server, flagged to generate a self-signed cert for dev/testing -Start-PodeServer { - - # bind to ip/port and set as https with self-signed cert - Add-PodeEndpoint -Address * -Port 8443 -Protocol Https -SelfSigned - #Add-PodeEndpoint -Address * -Port 8443 -Protocol Https -Certificate './certs/cert.pem' -CertificateKey './certs/key.pem' -CertificatePassword 'test' - #Add-PodeEndpoint -Address * -Port 8443 -Protocol Https -Certificate './certs/cert_nodes.pem' -CertificateKey './certs/key_nodes.pem' - #Add-PodeEndpoint -Address * -Port 8443 -Protocol Https -CertificateThumbprint '2A623A8DC46ED42A13B27DD045BFC91FDDAEB957' - - # set view engine for web pages - Set-PodeViewEngine -Type Pode - - # GET request for web page at "/" - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(1, 2, 3); } - } - - # GET request throws fake "500" server error status code - Add-PodeRoute -Method Get -Path '/error' -ScriptBlock { - Set-PodeResponseStatus -Code 500 - } - -} diff --git a/examples/web-pages-kestrel.ps1 b/examples/web-pages-kestrel.ps1 deleted file mode 100644 index aa73f7801..000000000 --- a/examples/web-pages-kestrel.ps1 +++ /dev/null @@ -1,98 +0,0 @@ -param( - [int] - $Port = 8085 -) - -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# you will require the Pode.Kestrel module for this example -Import-Module Pode.Kestrel - -# create a server, and start listening on port 8085 using kestrel -Start-PodeServer -Threads 2 -ListenerType Kestrel { - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8090 -Protocol Http -Name '8090Address' - Add-PodeEndpoint -Address * -Port $Port -Protocol Http -Name '8085Address' -RedirectTo '8090Address' - - # allow the local ip and some other ips - Add-PodeAccessRule -Access Allow -Type IP -Values @('127.0.0.1', '[::1]') - Add-PodeAccessRule -Access Allow -Type IP -Values @('192.169.0.1', '192.168.0.2') - - # deny an ip - Add-PodeAccessRule -Access Deny -Type IP -Values 10.10.10.10 - Add-PodeAccessRule -Access Deny -Type IP -Values '10.10.0.0/24' - Add-PodeAccessRule -Access Deny -Type IP -Values all - - # log requests to the terminal - New-PodeLoggingMethod -Terminal -Batch 10 -BatchTimeout 10 | Enable-PodeRequestLogging - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - # set view engine to pode renderer - Set-PodeViewEngine -Type Pode - - # wire up a custom logger - $logType = New-PodeLoggingMethod -Custom -ScriptBlock { - param($item) - $item.HttpMethod | Out-Default - } - - $logType | Add-PodeLogger -Name 'custom' -ScriptBlock { - param($item) - return @{ - HttpMethod = $item.HttpMethod - } - } - - # GET request for web page on "localhost:8085/" - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - # $WebEvent.Request | Write-PodeLog -Name 'custom' - Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(1, 2, 3); } - } - - # GET request throws fake "500" server error status code - Add-PodeRoute -Method Get -Path '/error' -ScriptBlock { - Set-PodeResponseStatus -Code 500 - } - - # GET request to page that merely redirects to google - Add-PodeRoute -Method Get -Path '/redirect' -ScriptBlock { - Move-PodeResponseUrl -Url 'https://google.com' - } - - # GET request that redirects to same host, just different port - Add-PodeRoute -Method Get -Path '/redirect-port' -ScriptBlock { - if ($WebEvent.Request.Url.Port -ne 8086) { - Move-PodeResponseUrl -Port 8086 - } - else { - Write-PodeJsonResponse -Value @{ 'value' = 'you got redirected!'; } - } - } - - # GET request to download a file - Add-PodeRoute -Method Get -Path '/download' -ScriptBlock { - Set-PodeResponseAttachment -Path 'Anger.jpg' - } - - # GET request with parameters - Add-PodeRoute -Method Get -Path '/:userId/details' -ScriptBlock { - Write-PodeJsonResponse -Value @{ 'userId' = $WebEvent.Parameters['userId'] } - } - - # ALL request, that supports every method and it a default drop route - Add-PodeRoute -Method * -Path '/all' -ScriptBlock { - Write-PodeJsonResponse -Value @{ 'value' = 'works for every http method' } - } - - Add-PodeRoute -Method Get -Path '/api/*/hello' -ScriptBlock { - Write-PodeJsonResponse -Value @{ 'value' = 'works for every hello route' } - } - - $hmm = 'well well' - Add-PodeRoute -Method Get -Path '/script' -FilePath './modules/route_script.ps1' - -} \ No newline at end of file diff --git a/examples/web-pages-md.ps1 b/examples/web-pages-md.ps1 deleted file mode 100644 index 4c778f958..000000000 --- a/examples/web-pages-md.ps1 +++ /dev/null @@ -1,21 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening on port 8090 -Start-PodeServer -Threads 2 { - - # listen on localhost:8090 - Add-PodeEndpoint -Address localhost -Port 8090 -Protocol Http - - # set view engine - Set-PodeViewEngine -Type Markdown - - # GET request for web page on "localhost:8090/" - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - Write-PodeViewResponse -Path 'index' - } - -} \ No newline at end of file diff --git a/examples/web-pages-simple.ps1 b/examples/web-pages-simple.ps1 deleted file mode 100644 index 893812910..000000000 --- a/examples/web-pages-simple.ps1 +++ /dev/null @@ -1,30 +0,0 @@ -param( - [int] - $Port = 8085 -) - -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening on port 8085 -Start-PodeServer -Threads 2 { - - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8090 -Protocol Http -Name '8090Address' - Add-PodeEndpoint -Address * -Port $Port -Protocol Http -Name '8085Address' -RedirectTo '8090Address' - - # log errors to the terminal - # New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - # set view engine to pode renderer - Set-PodeViewEngine -Type Pode - - # GET request for web page on "localhost:8085/" - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(1, 2, 3); } - } - -} \ No newline at end of file diff --git a/examples/web-rest-openapi-funcs.ps1 b/examples/web-rest-openapi-funcs.ps1 deleted file mode 100644 index c50b72343..000000000 --- a/examples/web-rest-openapi-funcs.ps1 +++ /dev/null @@ -1,19 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -Start-PodeServer { - Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http - - Enable-PodeOpenApi -Title 'OpenAPI Example' -RouteFilter '/api/*' -RestrictRoutes - Enable-PodeOpenApiViewer -Type Swagger -Path '/docs/swagger' - Enable-PodeOpenApiViewer -Type ReDoc -Path '/docs/redoc' - Enable-PodeOpenApiViewer -Type RapiDoc -Path '/docs/rapidoc' - Enable-PodeOpenApiViewer -Type StopLight -Path '/docs/stoplight' - Enable-PodeOpenApiViewer -Type Explorer -Path '/docs/explorer' - Enable-PodeOpenApiViewer -Type RapiPdf -Path '/docs/rapipdf' - - Enable-PodeOpenApiViewer -Bookmarks -Path '/docs' - - #ConvertTo-PodeRoute -Path '/api' -Commands @('Get-ChildItem', 'New-Item') - ConvertTo-PodeRoute -Path '/api' -Module Pester -} \ No newline at end of file diff --git a/examples/web-rest-openapi-simple.ps1 b/examples/web-rest-openapi-simple.ps1 deleted file mode 100644 index 072e7eba2..000000000 --- a/examples/web-rest-openapi-simple.ps1 +++ /dev/null @@ -1,50 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -Start-PodeServer { - Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http -Name 'user' - Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http -Name 'admin' - - Enable-PodeOpenApi -Title 'OpenAPI Example' -RouteFilter '/api/*' -RestrictRoutes - Enable-PodeOpenApiViewer -Type Swagger -DarkMode - Enable-PodeOpenApiViewer -Type ReDoc - Enable-PodeOpenApiViewer -Bookmarks -Path '/docs' - - Add-PodeRoute -Method Get -Path '/api/resources' -EndpointName 'user' -ScriptBlock { - Set-PodeResponseStatus -Code 200 - } - - - Add-PodeRoute -Method Post -Path '/api/resources' -ScriptBlock { - Set-PodeResponseStatus -Code 200 - } - - - Add-PodeRoute -Method Get -Path '/api/users/:userId' -ScriptBlock { - Write-PodeJsonResponse -Value @{ Name = 'Rick'; UserId = $WebEvent.Parameters['userId'] } - } -PassThru | Set-PodeOARouteInfo -PassThru | - Set-PodeOARequest -Parameters @( - (New-PodeOAIntProperty -Name 'userId' -Enum @(100, 300, 999) -Required | ConvertTo-PodeOAParameter -In Path) - ) - - - Add-PodeRoute -Method Get -Path '/api/users' -ScriptBlock { - Write-PodeJsonResponse -Value @{ Name = 'Rick'; UserId = $WebEvent.Query['userId'] } - } -PassThru | Set-PodeOARouteInfo -PassThru | - Set-PodeOARequest -Parameters @( - (New-PodeOAIntProperty -Name 'userId' -Required | ConvertTo-PodeOAParameter -In Query) - ) - - - Add-PodeRoute -Method Post -Path '/api/users' -ScriptBlock { - Write-PodeJsonResponse -Value @{ Name = $WebEvent.Data.Name; UserId = $WebEvent.Data.UserId } - } -PassThru | Set-PodeOARouteInfo -PassThru | - Set-PodeOARequest -RequestBody ( - New-PodeOARequestBody -Required -ContentSchemas @{ - 'application/json' = (New-PodeOAObjectProperty -Properties @( - (New-PodeOAStringProperty -Name 'Name' -MaxLength 5 -Pattern '[a-zA-Z]+'), - (New-PodeOAIntProperty -Name 'UserId') - )) - } - ) -} diff --git a/examples/web-route-endpoints.ps1 b/examples/web-route-endpoints.ps1 deleted file mode 100644 index b8e40fc80..000000000 --- a/examples/web-route-endpoints.ps1 +++ /dev/null @@ -1,37 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening on port 8080 and 8443 -Start-PodeServer { - - # listen on localhost:8080 - Add-PodeEndpoint -Address 127.0.0.1 -Port 8080 -Protocol Http -Name Endpoint1 - Add-PodeEndpoint -Address 127.0.0.2 -Port 8080 -Protocol Http -Name Endpoint2 - - # set view engine to pode - Set-PodeViewEngine -Type Pode - - # GET request for web page - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(1, 2, 3); } - } - - # GET request to download a file - Add-PodeRoute -Method Get -Path '/download' -ScriptBlock { - Set-PodeResponseAttachment -Path 'Anger.jpg' - } - - # GET request with parameters - Add-PodeRoute -Method Get -Path '/:userId/details' -ScriptBlock { - Write-PodeJsonResponse -Value @{ 'userId' = $WebEvent.Parameters['userId'] } - } - - # ALL requests for 127.0.0.2 to 127.0.0.1 - Add-PodeRoute -Method * -Path * -EndpointName Endpoint2 -ScriptBlock { - Move-PodeResponseUrl -Address 127.0.0.1 - } - -} \ No newline at end of file diff --git a/examples/web-route-group.ps1 b/examples/web-route-group.ps1 deleted file mode 100644 index 166746465..000000000 --- a/examples/web-route-group.ps1 +++ /dev/null @@ -1,67 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -$message = 'Kenobi' - -# create a server, and start listening on port 8090 -Start-PodeServer -Threads 2 { - - # listen on localhost:8090 - Add-PodeEndpoint -Address localhost -Port 8090 -Protocol Http - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - $mid1 = New-PodeMiddleware -ScriptBlock { - 'here1' | Out-Default - } - - $mid2 = New-PodeMiddleware -ScriptBlock { - 'here2' | Out-Default - } - - Add-PodeRouteGroup -Path '/api' -Middleware $mid1 -Routes { - Add-PodeRoute -Method Get -Path '/route1' -ScriptBlock { - Write-PodeJsonResponse -Value @{ ID = 1 } - } - - Add-PodeRouteGroup -Path '/inner' -Routes { - Add-PodeRoute -Method Get -Path '/route2' -Middleware $using:mid2 -ScriptBlock { - Write-PodeJsonResponse -Value @{ ID = 2 } - } - - Add-PodeRoute -Method Get -Path '/route3' -ScriptBlock { - "Hello there, $($using:message)" | Out-Default - Write-PodeJsonResponse -Value @{ ID = 3 } - } - } - } - - - # Invoke-RestMethod -Uri http://localhost:8090/auth/route1 -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' } - New-PodeAuthScheme -Basic | Add-PodeAuth -Name 'Basic' -Sessionless -ScriptBlock { - param($username, $password) - - # here you'd check a real user storage, this is just for example - if ($username -eq 'morty' -and $password -eq 'pickle') { - return @{ - User = @{ ID = 'M0R7Y302' } - } - } - - return @{ Message = 'Invalid details supplied' } - } - - Add-PodeRouteGroup -Path '/auth' -Authentication Basic -Routes { - Add-PodeRoute -Method Post -Path '/route1' -ScriptBlock { - Write-PodeJsonResponse -Value @{ ID = 1 } - } - - Add-PodeRoute -Method Post -Path '/route2' -ScriptBlock { - Write-PodeJsonResponse -Value @{ ID = 2 } - } - - Add-PodeRoute -Method Post -Path '/route3' -ScriptBlock { - Write-PodeJsonResponse -Value @{ ID = 3 } - } - } - -} \ No newline at end of file diff --git a/examples/web-route-listen-names.ps1 b/examples/web-route-listen-names.ps1 deleted file mode 100644 index d15eabb9b..000000000 --- a/examples/web-route-listen-names.ps1 +++ /dev/null @@ -1,44 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening on port 8080 and 8443 -Start-PodeServer { - - # listen on localhost:8080/8443 - Add-PodeEndpoint -Address 127.0.0.1 -Port 8080 -Protocol Http -Name 'local1' - Add-PodeEndpoint -Address 127.0.0.2 -Port 8080 -Protocol Http -Name 'local2' - Add-PodeEndpoint -Address 127.0.0.3 -Port 8080 -Protocol Http -Name 'local3' - Add-PodeEndpoint -Address 127.0.0.4 -Port 8080 -Protocol Http -Name 'local4' - - # set view engine to pode - Set-PodeViewEngine -Type Pode - - # GET request for web page - all endpoints - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(1, 2, 3); } - } - - # GET request for web page - local2 endpoint - Add-PodeRoute -Method Get -Path '/' -EndpointName 'local2' -ScriptBlock { - Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(1, 2, 3, 4, 5, 6, 7, 8); } - } - - # GET request for web page - local3 and local4 endpoints - Add-PodeRoute -Method Get -Path '/' -EndpointName 'local3', 'local4' -ScriptBlock { - Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(2, 4, 6, 8, 10, 12, 14, 16); } - } - - # GET request to download a file - Add-PodeRoute -Method Get -Path '/download' -ScriptBlock { - Set-PodeResponseAttachment -Path 'Anger.jpg' - } - - # GET request with parameters - Add-PodeRoute -Method Get -Path '/:userId/details' -ScriptBlock { - Write-PodeJsonResponse -Value @{ 'userId' = $WebEvent.Parameters['userId'] } - } - -} \ No newline at end of file diff --git a/examples/web-route-protocols.ps1 b/examples/web-route-protocols.ps1 deleted file mode 100644 index 6cc26e05b..000000000 --- a/examples/web-route-protocols.ps1 +++ /dev/null @@ -1,37 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening on port 8080 and 8443 -Start-PodeServer { - - # listen on localhost:8080/8443 - Add-PodeEndpoint -Address * -Port 8080 -Protocol Http -Name Endpoint1 - Add-PodeEndpoint -Address * -Port 8443 -Protocol Https -Name Endpoint2 -SelfSigned - - # set view engine to pode - Set-PodeViewEngine -Type Pode - - # GET request for web page - Add-PodeRoute -Method Get -Path '/' -EndpointName Endpoint2 -ScriptBlock { - Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(1, 2, 3); } - } - - # GET request to download a file - Add-PodeRoute -Method Get -Path '/download' -ScriptBlock { - Set-PodeResponseAttachment -Path 'Anger.jpg' - } - - # GET request with parameters - Add-PodeRoute -Method Get -Path '/:userId/details' -ScriptBlock { - Write-PodeJsonResponse -Value @{ 'userId' = $WebEvent.Parameters['userId'] } - } - - # ALL requests for http only to redirect to https - Add-PodeRoute -Method * -Path * -EndpointName Endpoint1 -ScriptBlock { - Move-PodeResponseUrl -Protocol Https -Port 8443 - } - -} \ No newline at end of file diff --git a/examples/web-secrets-local.ps1 b/examples/web-secrets-local.ps1 deleted file mode 100644 index 259dfb9e8..000000000 --- a/examples/web-secrets-local.ps1 +++ /dev/null @@ -1,55 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# make sure to install the Microsoft.PowerShell.SecretStore modules! -# Install-Module Microsoft.PowerShell.SecretManagement, Microsoft.PowerShell.SecretStore - -Start-PodeServer -Threads 2 { - # listen - Add-PodeEndpoint -Address * -Port 8080 -Protocol Http - - # logging - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - - # secret manage local vault - $params = @{ - Name = 'PodeTest_LocalVault' - ModuleName = 'Microsoft.PowerShell.SecretStore' - UnlockSecret = 'Sup3rSecur3Pa$$word!' - } - - Register-PodeSecretVault @params - - - # set a secret in the local vault - Set-PodeSecret -Key 'hello' -Vault 'PodeTest_LocalVault' -InputObject 'world' - - - # mount a secret from local vault - Mount-PodeSecret -Name 'hello' -Vault 'PodeTest_LocalVault' -Key 'hello' - - - # routes to get/update secret in local vault - Add-PodeRoute -Method Get -Path '/module' -ScriptBlock { - Write-PodeJsonResponse @{ Value = $secret:hello } - } - - Add-PodeRoute -Method Post -Path '/module' -ScriptBlock { - $secret:hello = $WebEvent.Data.Value - } - - - Add-PodeRoute -Method Post -Path '/adhoc/:key' -ScriptBlock { - Set-PodeSecret -Key $WebEvent.Parameters['key'] -Vault 'PodeTest_LocalVault' -InputObject $WebEvent.Data['value'] - Mount-PodeSecret -Name $WebEvent.Data['name'] -Vault 'PodeTest_LocalVault' -Key $WebEvent.Parameters['key'] - } - - Add-PodeRoute -Method Delete -Path '/adhoc/:key' -ScriptBlock { - Remove-PodeSecret -Key $WebEvent.Parameters['key'] -Vault 'PodeTest_LocalVault' - Dismount-PodeSecret -Name $WebEvent.Parameters['key'] - } -} diff --git a/examples/web-sessions.ps1 b/examples/web-sessions.ps1 deleted file mode 100644 index 833427c01..000000000 --- a/examples/web-sessions.ps1 +++ /dev/null @@ -1,27 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening on port 8085 -Start-PodeServer { - - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http - - # set view engine to pode renderer - Set-PodeViewEngine -Type Pode - - # setup session details - Enable-PodeSessionMiddleware -Duration 120 -Extend -Generator { - return [System.IO.Path]::GetRandomFileName() - } - - # GET request for web page on "localhost:8085/" - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - $WebEvent.Session.Data.Views++ - Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @($WebEvent.Session.Data.Views); } - } - -} \ No newline at end of file diff --git a/examples/web-signal-connection.ps1 b/examples/web-signal-connection.ps1 deleted file mode 100644 index 8aace85a2..000000000 --- a/examples/web-signal-connection.ps1 +++ /dev/null @@ -1,38 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening -Start-PodeServer -EnablePool WebSockets { - - # listen - Add-PodeEndpoint -Address * -Port 8092 -Protocol Http - - # log requests to the terminal - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging -Level Error, Debug, Verbose - - # connect to web socket from web-signal.ps1 - Connect-PodeWebSocket -Name 'Example' -Url 'ws://localhost:8091' -ScriptBlock { - $WsEvent.Data | Out-Default - if ($WsEvent.Data.message -inotlike '*Ex:*') { - Send-PodeWebSocket -Message @{ message = "Ex: $($WsEvent.Data.message)" } - } - } - - # Add-PodeRoute -Method Get -Path '/connect' -ScriptBlock { - # Connect-PodeWebSocket -Name 'Test' -Url 'wss://ws.ifelse.io/' -ScriptBlock { - # $WsEvent.Request | out-default - # } - # } - - # Add-PodeTimer -Name 'Test' -Interval 10 -ScriptBlock { - # $rand = Get-Random -Minimum 10 -Maximum 1000 - # Send-PodeWebSocket -Name 'Test' -Message "hello $rand" - # } - - # Add-PodeRoute -Method Get -Path '/reset' -ScriptBlock { - # Reset-PodeWebSocket -Name 'Example' - # } -} \ No newline at end of file diff --git a/examples/web-signal.ps1 b/examples/web-signal.ps1 deleted file mode 100644 index ab305253c..000000000 --- a/examples/web-signal.ps1 +++ /dev/null @@ -1,37 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening -Start-PodeServer -Threads 3 { - - # listen - Add-PodeEndpoint -Address * -Port 8091 -Protocol Http - Add-PodeEndpoint -Address * -Port 8091 -Protocol Ws - #Add-PodeEndpoint -Address * -Port 8090 -Certificate './certs/pode-cert.pfx' -CertificatePassword '1234' -Protocol Https - #Add-PodeEndpoint -Address * -Port 8091 -Certificate './certs/pode-cert.pfx' -CertificatePassword '1234' -Protocol Wss - - # log requests to the terminal - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging -Level Error, Debug, Verbose - - # set view engine to pode renderer - Set-PodeViewEngine -Type Html - - # GET request for web page - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - Write-PodeViewResponse -Path 'websockets' - } - - # SIGNAL route, to return current date - Add-PodeSignalRoute -Path '/' -ScriptBlock { - $msg = $SignalEvent.Data.Message - - if ($msg -ieq '[date]') { - $msg = [datetime]::Now.ToString() - } - - Send-PodeSignal -Value @{ message = $msg } - } -} \ No newline at end of file diff --git a/examples/web-simple-pages.ps1 b/examples/web-simple-pages.ps1 deleted file mode 100644 index a0d745542..000000000 --- a/examples/web-simple-pages.ps1 +++ /dev/null @@ -1,16 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# create a server, and start listening on port 8090 -Start-PodeServer -Threads 2 { - - # listen on localhost:8090 - Add-PodeEndpoint -Address localhost -Port 8090 -Protocol Http - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - Add-PodePage -Name Processes -ScriptBlock { Get-Process } - Add-PodePage -Name Services -ScriptBlock { Get-Service } - Add-PodePage -Name Index -View 'simple' - Add-PodePage -Name File -FilePath '.\views\simple.pode' -Data @{ 'numbers' = @(1, 2, 3); } - -} \ No newline at end of file diff --git a/examples/web-sockets.ps1 b/examples/web-sockets.ps1 deleted file mode 100644 index 7a899d23a..000000000 --- a/examples/web-sockets.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening -Start-PodeServer -Threads 5 { - - # listen - Add-PodeEndpoint -Address * -Port 8090 -Certificate './certs/pode-cert.pfx' -CertificatePassword '1234' -Protocol Https - # Add-PodeEndpoint -Address * -Port 8090 -SelfSigned -Protocol Https - - # log requests to the terminal - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - # GET request for web page on "localhost:8085/" - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - Write-PodeJsonResponse -Value @{ - Kenobi = 'Hello, there' - } - } -} \ No newline at end of file diff --git a/examples/web-static-auth.ps1 b/examples/web-static-auth.ps1 deleted file mode 100644 index b065771b0..000000000 --- a/examples/web-static-auth.ps1 +++ /dev/null @@ -1,53 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -if (Test-Path -Path "$($path)/src/Pode.psm1" -PathType Leaf) { - Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop -} -else { - Import-Module -Name 'Pode' -} - -# create a server, and start listening on port 8085 -Start-PodeServer -Threads 2 { - - # listen on localhost:8085 - Add-PodeEndpoint -Address localhost -Port 8085 -Protocol Http - - New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - # setup basic auth (base64> username:password in header) - New-PodeAuthScheme -Basic -Realm 'Pode Static Page' | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { - param($username, $password) - - # here you'd check a real user storage, this is just for example - if ($username -eq 'morty' -and $password -eq 'pickle') { - return @{ - User = @{ - ID ='M0R7Y302' - Name = 'Morty' - Type = 'Human' - } - } - } - - return @{ Message = 'Invalid details supplied' } - } - - # set view engine to pode renderer - Set-PodeViewEngine -Type Pode - - # STATIC asset folder route - Add-PodeStaticRoute -Path '/assets' -Source './assets' -Defaults @('index.html') -Authentication 'Validate' - Add-PodeStaticRoute -Path '/assets/download' -Source './assets' -DownloadOnly - - # GET request for web page on "localhost:8085/" - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - Write-PodeViewResponse -Path 'web-static' -Data @{ 'numbers' = @(1, 2, 3); } - } - - # GET request to download a file from static route - Add-PodeRoute -Method Get -Path '/download' -ScriptBlock { - Set-PodeResponseAttachment -Path '/assets/images/Fry.png' - } - -} \ No newline at end of file diff --git a/examples/web-static.ps1 b/examples/web-static.ps1 deleted file mode 100644 index c6b4a0cc0..000000000 --- a/examples/web-static.ps1 +++ /dev/null @@ -1,37 +0,0 @@ -param( - [int] - $Port = 8085 -) - -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening on port 8085 -Start-PodeServer -Threads 2 { - - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port $port -Protocol Http - New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - # set view engine to pode renderer - Set-PodeViewEngine -Type Pode - - # STATIC asset folder route - Add-PodeStaticRoute -Path '/assets' -Source './assets' -Defaults @('index.html') - Add-PodeStaticRoute -Path '/assets/download' -Source './assets' -DownloadOnly - - # GET request for web page on "localhost:8085/" - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - Write-PodeViewResponse -Path 'web-static' -Data @{ 'numbers' = @(1, 2, 3); } - } - - # GET request to download a file from static route - Add-PodeRoute -Method Get -Path '/download' -ScriptBlock { - Set-PodeResponseAttachment -Path '/assets/images/Fry.png' - } - -} \ No newline at end of file diff --git a/examples/web-tp-eps.ps1 b/examples/web-tp-eps.ps1 deleted file mode 100644 index 7cb422935..000000000 --- a/examples/web-tp-eps.ps1 +++ /dev/null @@ -1,36 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -Import-Module -Name EPS - -# create a server, and start listening on port 8085 -Start-PodeServer -Threads 2 { - - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http - - # log requests to the terminal - New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging - - # set view engine to EPS renderer - Set-PodeViewEngine -Type EPS -ScriptBlock { - param($path, $data) - $template = Get-Content -Path $path -Raw -Force - - if ($null -eq $data) { - return (Invoke-EpsTemplate -Template $template) - } - else { - return (Invoke-EpsTemplate -Template $template -Binding $data) - } - } - - # GET request for web page on "localhost:8085/" - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - Write-PodeViewResponse -Path 'index' -Data @{ 'numbers' = @(1, 2, 3); 'date' = [DateTime]::UtcNow; } - } - -} \ No newline at end of file diff --git a/examples/web-tp-pshtml.ps1 b/examples/web-tp-pshtml.ps1 deleted file mode 100644 index 87d25dff8..000000000 --- a/examples/web-tp-pshtml.ps1 +++ /dev/null @@ -1,29 +0,0 @@ -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -Import-Module -Name PSHTML - -# create a server, and start listening on port 8085 -Start-PodeServer -Threads 2 { - - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http - - # log requests to the terminal - New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging - - # set view engine to PSHTML renderer - Set-PodeViewEngine -Type PSHTML -Extension PS1 -ScriptBlock { - param($path, $data) - return [string](. $path $data) - } - - # GET request for web page on "localhost:8085/" - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - Write-PodeViewResponse -Path 'index' -Data @{ 'numbers' = @(1, 2, 3); } - } - -} \ No newline at end of file diff --git a/examples/web-upload-kestrel.ps1 b/examples/web-upload-kestrel.ps1 deleted file mode 100644 index 61560dae2..000000000 --- a/examples/web-upload-kestrel.ps1 +++ /dev/null @@ -1,32 +0,0 @@ -param( - [int] - $Port = 8085 -) - -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode -Import-Module Pode.Kestrel - -# create a server, and start listening on port 8085 -Start-PodeServer -Threads 2 -ListenerType Kestrel { - - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port $port -Protocol Http - - Set-PodeViewEngine -Type HTML - - # GET request for web page on "localhost:8085/" - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - Write-PodeViewResponse -Path 'web-upload' - } - - # POST request to upload a file - Add-PodeRoute -Method Post -Path '/upload' -ScriptBlock { - Save-PodeRequestFile -Key 'avatar' - Move-PodeResponseUrl -Url '/' - } - -} \ No newline at end of file diff --git a/examples/web-upload.ps1 b/examples/web-upload.ps1 deleted file mode 100644 index e652c3ffb..000000000 --- a/examples/web-upload.ps1 +++ /dev/null @@ -1,43 +0,0 @@ -param( - [int] - $Port = 8085 -) - -$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode - -# create a server, and start listening on port 8085 -Start-PodeServer -Threads 2 { - - # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port $port -Protocol Http - - Set-PodeViewEngine -Type HTML - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - - # GET request for web page on "localhost:8085/" - Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - Write-PodeViewResponse -Path 'web-upload' - } - - # POST request to upload a file - Add-PodeRoute -Method Post -Path '/upload' -ScriptBlock { - Save-PodeRequestFile -Key 'avatar' - Move-PodeResponseUrl -Url '/' - } - - # GET request for web page on "localhost:8085/multi" - Add-PodeRoute -Method Get -Path '/multi' -ScriptBlock { - Write-PodeViewResponse -Path 'web-upload-multi' - } - - # POST request to upload multiple files - Add-PodeRoute -Method Post -Path '/upload-multi' -ScriptBlock { - Save-PodeRequestFile -Key 'avatar' -Path 'C:/temp' -FileName 'Ruler.png' - Move-PodeResponseUrl -Url '/multi' - } - -} \ No newline at end of file diff --git a/packers/choco/tools/ChocolateyInstall_template.ps1 b/packers/choco/tools/ChocolateyInstall_template.ps1 index d3d6dc08e..e2553172c 100644 --- a/packers/choco/tools/ChocolateyInstall_template.ps1 +++ b/packers/choco/tools/ChocolateyInstall_template.ps1 @@ -24,7 +24,7 @@ function Install-PodeModule($path, $version) { Push-Location (Join-Path $toolsDir 'src') # which folders do we need? - $folders = @('Private', 'Public', 'Misc', 'Libs', 'licenses') + $folders = @('Private', 'Public', 'Misc', 'Libs', 'licenses','Locales') $folders | ForEach-Object { New-Item -ItemType Directory -Path (Join-Path $path $_) -Force | Out-Null Copy-Item -Path "./$($_)/*" -Destination (Join-Path $path $_) -Force -Recurse | Out-Null diff --git a/pode.build.ps1 b/pode.build.ps1 index 6fa324e7b..2d2be077d 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -1,5 +1,9 @@ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingCmdletAliases', '')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingInvokeExpression', '')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUSeDeclaredVarsMoreThanAssignments', '')] param( [string] $Version = '0.0.0', @@ -12,14 +16,20 @@ param( $PowerShellVersion = 'lts', [string] - $ReleaseNoteVersion + $ReleaseNoteVersion, + + [string] + $UICulture = 'en-US' ) +# Fix for PS7.5 Preview - https://github.com/PowerShell/PowerShell/issues/23868 +$ProgressPreference = 'SilentlyContinue' + <# # Dependency Versions #> $Versions = @{ - Pester = '5.5.0' + Pester = '5.6.1' MkDocs = '1.6.0' PSCoveralls = '1.0.0' SevenZip = '18.5.0.20180730' @@ -334,6 +344,43 @@ Task DocsDeps ChocoDeps, { Install-PodeBuildModule PlatyPS } +Task IndexSamples { + $examplesPath = './examples' + if (!(Test-Path -PathType Container -Path $examplesPath)) { + return + } + + # List of directories to exclude + $sampleMarkDownPath = './docs/Getting-Started/Samples.md' + $excludeDirs = @('scripts', 'views', 'static', 'public', 'assets', 'timers', 'modules', + 'Authentication', 'certs', 'logs', 'relative', 'routes', 'issues') + + # Convert exlusion list into single regex pattern for directory matching + $dirSeparator = [IO.Path]::DirectorySeparatorChar + $excludeDirs = "\$($dirSeparator)($($excludeDirs -join '|'))\$($dirSeparator)" + + # build the page content + Get-ChildItem -Path $examplesPath -Filter *.ps1 -Recurse -File -Force | + Where-Object { + $_.FullName -inotmatch $excludeDirs + } | + Sort-Object -Property FullName | + ForEach-Object { + Write-Verbose "Processing Sample: $($_.FullName)" + + # get the script help + $help = Get-Help -Name $_.FullName -ErrorAction Stop + + # add help content + $urlFileName = ($_.FullName -isplit 'examples')[1].Trim('\/') -replace '[\\/]', '/' + $markdownContent += "## [$($_.BaseName)](https://github.com/Badgerati/Pode/blob/develop/examples/$($urlFileName))`n`n" + $markdownContent += "**Synopsis**`n`n$($help.Synopsis)`n`n" + $markdownContent += "**Description**`n`n$($help.Description.Text)`n`n" + } + + Write-Output "Write Markdown document for the sample files to $($sampleMarkDownPath)" + Set-Content -Path $sampleMarkDownPath -Value "# Sample Scripts`n`n$($markdownContent)" -Force +} <# # Building @@ -427,7 +474,7 @@ Task Pack Build, { New-Item -Path $path -ItemType Directory -Force | Out-Null # which source folders do we need? create them and copy their contents - $folders = @('Private', 'Public', 'Misc', 'Libs') + $folders = @('Private', 'Public', 'Misc', 'Libs', 'Locales') $folders | ForEach-Object { New-Item -ItemType Directory -Path (Join-Path $path $_) -Force | Out-Null Copy-Item -Path "./src/$($_)/*" -Destination (Join-Path $path $_) -Force -Recurse | Out-Null @@ -464,7 +511,13 @@ Task TestNoBuild TestDeps, { if (Test-PodeBuildIsWindows) { netsh int ipv4 show excludedportrange protocol=tcp | Out-Default } - + if ($UICulture -ne ([System.Threading.Thread]::CurrentThread.CurrentUICulture) ) { + $originalUICulture = [System.Threading.Thread]::CurrentThread.CurrentUICulture + Write-Output "Original UICulture is $originalUICulture" + Write-Output "Set UICulture to $UICulture" + # set new UICulture + [System.Threading.Thread]::CurrentThread.CurrentUICulture = $UICulture + } $Script:TestResultFile = "$($pwd)/TestResults.xml" # get default from static property @@ -485,12 +538,16 @@ Task TestNoBuild TestDeps, { else { $Script:TestStatus = Invoke-Pester -Configuration $configuration } + if ($originalUICulture) { + Write-Output "Restore UICulture to $originalUICulture" + # restore original UICulture + [System.Threading.Thread]::CurrentThread.CurrentUICulture = $originalUICulture + } }, PushCodeCoverage, CheckFailedTests # Synopsis: Run tests after a build Task Test Build, TestNoBuild - # Synopsis: Check if any of the tests failed Task CheckFailedTests { if ($TestStatus.FailedCount -gt 0) { @@ -524,7 +581,7 @@ Task Docs DocsDeps, DocsHelpBuild, { } # Synopsis: Build the function help documentation -Task DocsHelpBuild DocsDeps, Build, { +Task DocsHelpBuild IndexSamples, DocsDeps, Build, { # import the local module Remove-Module Pode -Force -ErrorAction Ignore | Out-Null Import-Module ./src/Pode.psm1 -Force | Out-Null @@ -578,7 +635,7 @@ Task DocsBuild DocsDeps, DocsHelpBuild, { #> # Synopsis: Clean the build enviroment -Task Clean CleanPkg, CleanDeliverable, CleanLibs, CleanListener +Task Clean CleanPkg, CleanDeliverable, CleanLibs, CleanListener, CleanDocs # Synopsis: Clean the Deliverable folder Task CleanDeliverable { @@ -633,7 +690,13 @@ Task CleanListener { Write-Host "Cleanup $path done" } - +Task CleanDocs { + $path = './docs/Getting-Started/Samples.md' + if (Test-Path -Path $path -PathType Leaf) { + Write-Host "Removing $path" + Remove-Item -Path $path -Force | Out-Null + } +} <# # Local module management #> @@ -652,7 +715,7 @@ Task Install-Module -If ($Version) Pack, { $path = './pkg' # copy over folders - $folders = @('Private', 'Public', 'Misc', 'Libs', 'licenses') + $folders = @('Private', 'Public', 'Misc', 'Libs', 'licenses', 'Locales') $folders | ForEach-Object { Copy-Item -Path (Join-Path -Path $path -ChildPath $_) -Destination $dest -Force -Recurse | Out-Null } @@ -827,6 +890,10 @@ task ReleaseNotes { $dependabot = @{} foreach ($pr in $prs) { + if ($pr.labels.name -icontains 'superseded') { + continue + } + $label = ($pr.labels[0].name -split ' ')[0] if ($label -iin @('new-release', 'internal-code')) { continue @@ -873,7 +940,7 @@ task ReleaseNotes { } } - $titles = @($pr.title) + $titles = @($pr.title).Trim() if ($pr.title.Contains(';')) { $titles = ($pr.title -split ';').Trim() } @@ -884,7 +951,7 @@ task ReleaseNotes { } foreach ($title in $titles) { - $str = "* #$($pr.number): $($title)" + $str = "* #$($pr.number): $($title -replace '`', "'")" if (![string]::IsNullOrWhiteSpace($author)) { $str += " (thanks @$($author)!)" } @@ -923,4 +990,4 @@ task ReleaseNotes { $categories[$category] | Sort-Object | ForEach-Object { Write-Host $_ } Write-Host '' } -} +} \ No newline at end of file diff --git a/src/Listener/NuGet.config b/src/Listener/NuGet.config new file mode 100644 index 000000000..83a2e372f --- /dev/null +++ b/src/Listener/NuGet.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/Listener/Pode.csproj b/src/Listener/Pode.csproj index ad20a3158..3932c1dc7 100644 --- a/src/Listener/Pode.csproj +++ b/src/Listener/Pode.csproj @@ -2,5 +2,6 @@ netstandard2.0;net6.0;net8.0 $(NoWarn);SYSLIB0001 + 7.3 diff --git a/src/Listener/PodeConnector.cs b/src/Listener/PodeConnector.cs index be7a3d5a8..b75aff220 100644 --- a/src/Listener/PodeConnector.cs +++ b/src/Listener/PodeConnector.cs @@ -15,9 +15,8 @@ public class PodeConnector : IDisposable { CancellationToken = cancellationToken == default(CancellationToken) ? cancellationToken - : (new CancellationTokenSource()).Token; + : new CancellationTokenSource().Token; - // IsConnected = true; IsDisposed = false; } diff --git a/src/Listener/PodeContext.cs b/src/Listener/PodeContext.cs index ebc00fdca..52af8cd66 100644 --- a/src/Listener/PodeContext.cs +++ b/src/Listener/PodeContext.cs @@ -5,6 +5,7 @@ using System.Net.Sockets; using System.Security.Cryptography; using System.Threading; +using System.Threading.Tasks; namespace Pode { @@ -18,11 +19,7 @@ public class PodeContext : PodeProtocol, IDisposable public PodeSocket PodeSocket { get; private set; } public DateTime Timestamp { get; private set; } public Hashtable Data { get; private set; } - - public string EndpointName - { - get => PodeSocket.Name; - } + public string EndpointName => PodeSocket.Name; private object _lockable = new object(); @@ -39,73 +36,25 @@ private set } } - public bool CloseImmediately - { - get => (State == PodeContextState.Error + public bool CloseImmediately => State == PodeContextState.Error || State == PodeContextState.Closing || State == PodeContextState.Timeout - || Request.CloseImmediately); - } - - public new bool IsWebSocket - { - get => (base.IsWebSocket || (base.IsUnknown && PodeSocket.IsWebSocket)); - } - - public bool IsWebSocketUpgraded - { - get => (IsWebSocket && Request is PodeSignalRequest); - } - - public new bool IsSmtp - { - get => (base.IsSmtp || (base.IsUnknown && PodeSocket.IsSmtp)); - } - - public new bool IsHttp - { - get => (base.IsHttp || (base.IsUnknown && PodeSocket.IsHttp)); - } - - public PodeSmtpRequest SmtpRequest - { - get => (PodeSmtpRequest)Request; - } - - public PodeHttpRequest HttpRequest - { - get => (PodeHttpRequest)Request; - } + || Request.CloseImmediately; - public PodeSignalRequest SignalRequest - { - get => (PodeSignalRequest)Request; - } - - public bool IsKeepAlive - { - get => ((Request.IsKeepAlive && Response.SseScope != PodeSseScope.Local) || Response.SseScope == PodeSseScope.Global); - } - - public bool IsErrored - { - get => (State == PodeContextState.Error || State == PodeContextState.SslError); - } + public new bool IsWebSocket => base.IsWebSocket || (IsUnknown && PodeSocket.IsWebSocket); + public bool IsWebSocketUpgraded => IsWebSocket && Request is PodeSignalRequest; + public new bool IsSmtp => base.IsSmtp || (IsUnknown && PodeSocket.IsSmtp); + public new bool IsHttp => base.IsHttp || (IsUnknown && PodeSocket.IsHttp); - public bool IsTimeout - { - get => (State == PodeContextState.Timeout); - } + public PodeSmtpRequest SmtpRequest => (PodeSmtpRequest)Request; + public PodeHttpRequest HttpRequest => (PodeHttpRequest)Request; + public PodeSignalRequest SignalRequest => (PodeSignalRequest)Request; - public bool IsClosed - { - get => (State == PodeContextState.Closed); - } - - public bool IsOpened - { - get => (State == PodeContextState.Open); - } + public bool IsKeepAlive => (Request.IsKeepAlive && Response.SseScope != PodeSseScope.Local) || Response.SseScope == PodeSseScope.Global; + public bool IsErrored => State == PodeContextState.Error || State == PodeContextState.SslError; + public bool IsTimeout => State == PodeContextState.Timeout; + public bool IsClosed => State == PodeContextState.Closed; + public bool IsOpened => State == PodeContextState.Open; public CancellationTokenSource ContextTimeoutToken { get; private set; } private Timer TimeoutTimer; @@ -121,14 +70,17 @@ public PodeContext(Socket socket, PodeSocket podeSocket, PodeListener listener) Type = PodeProtocolType.Unknown; State = PodeContextState.New; + } + public async Task Initialise() + { NewResponse(); - NewRequest(); + await NewRequest().ConfigureAwait(false); } private void TimeoutCallback(object state) { - if (Response.SseEnabled) + if (Response.SseEnabled || Request.IsWebSocket) { return; } @@ -140,54 +92,51 @@ private void TimeoutCallback(object state) Request.Error = new HttpRequestException("Request timeout"); Request.Error.Data.Add("PodeStatusCode", 408); - this.Dispose(); + Dispose(); } private void NewResponse() { - Response = new PodeResponse(); - Response.SetContext(this); + Response = new PodeResponse(this); } - private void NewRequest() + private async Task NewRequest() { // create a new request switch (PodeSocket.Type) { case PodeProtocolType.Smtp: - Request = new PodeSmtpRequest(Socket, PodeSocket); + Request = new PodeSmtpRequest(Socket, PodeSocket, this); break; case PodeProtocolType.Tcp: - Request = new PodeTcpRequest(Socket, PodeSocket); + Request = new PodeTcpRequest(Socket, PodeSocket, this); break; default: - Request = new PodeHttpRequest(Socket, PodeSocket); + Request = new PodeHttpRequest(Socket, PodeSocket, this); break; } - Request.SetContext(this); - // attempt to open the request stream try { - Request.Open(); + await Request.Open(CancellationToken.None).ConfigureAwait(false); State = PodeContextState.Open; } catch (AggregateException aex) { PodeHelpers.HandleAggregateException(aex, Listener, PodeLoggingLevel.Debug, true); - State = (Request.InputStream == default(Stream) + State = Request.InputStream == default(Stream) ? PodeContextState.Error - : PodeContextState.SslError); + : PodeContextState.SslError; } catch (Exception ex) { PodeHelpers.WriteException(ex, Listener, PodeLoggingLevel.Debug); - State = (Request.InputStream == default(Stream) + State = Request.InputStream == default(Stream) ? PodeContextState.Error - : PodeContextState.SslError); + : PodeContextState.SslError; } // if request is SMTP or TCP, send ACK if available @@ -195,11 +144,11 @@ private void NewRequest() { if (PodeSocket.IsSmtp) { - SmtpRequest.SendAck(); + await SmtpRequest.SendAck().ConfigureAwait(false); } else if (PodeSocket.IsTcp && !string.IsNullOrWhiteSpace(PodeSocket.AcknowledgeMessage)) { - Response.WriteLine(PodeSocket.AcknowledgeMessage, true); + await Response.WriteLine(PodeSocket.AcknowledgeMessage, true).ConfigureAwait(false); } } } @@ -261,30 +210,27 @@ private void SetContextType() } } - public void RenewTimeoutToken() - { - ContextTimeoutToken = new CancellationTokenSource(); - } - public void CancelTimeout() { TimeoutTimer.Dispose(); } - public async void Receive() + public async Task Receive() { try { // start timeout + ContextTimeoutToken = new CancellationTokenSource(); TimeoutTimer = new Timer(TimeoutCallback, null, Listener.RequestTimeout * 1000, Timeout.Infinite); // start receiving State = PodeContextState.Receiving; try { - var close = await Request.Receive(ContextTimeoutToken.Token); + PodeHelpers.WriteErrorMessage($"Receiving request", Listener, PodeLoggingLevel.Verbose, this); + var close = await Request.Receive(ContextTimeoutToken.Token).ConfigureAwait(false); SetContextType(); - EndReceive(close); + await EndReceive(close).ConfigureAwait(false); } catch (OperationCanceledException) { } } @@ -292,11 +238,11 @@ public async void Receive() { PodeHelpers.WriteException(ex, Listener, PodeLoggingLevel.Debug); State = PodeContextState.Error; - PodeSocket.HandleContext(this); + await PodeSocket.HandleContext(this).ConfigureAwait(false); } } - public void EndReceive(bool close) + public async Task EndReceive(bool close) { State = close ? PodeContextState.Closing : PodeContextState.Received; if (close) @@ -304,7 +250,7 @@ public void EndReceive(bool close) Response.StatusCode = 400; } - PodeSocket.HandleContext(this); + await PodeSocket.HandleContext(this).ConfigureAwait(false); } public void StartReceive() @@ -315,7 +261,7 @@ public void StartReceive() PodeHelpers.WriteErrorMessage($"Socket listening", Listener, PodeLoggingLevel.Verbose, this); } - public void UpgradeWebSocket(string clientId = null) + public async Task UpgradeWebSocket(string clientId = null) { PodeHelpers.WriteErrorMessage($"Upgrading Websocket", Listener, PodeLoggingLevel.Verbose, this); @@ -354,7 +300,7 @@ public void UpgradeWebSocket(string clientId = null) } // send message to upgrade web socket - Response.Send(); + await Response.Send().ConfigureAwait(false); // add open web socket to listener var signal = new PodeSignal(this, HttpRequest.Url.AbsolutePath, clientId); @@ -372,10 +318,12 @@ public void Dispose(bool force) { lock (_lockable) { + PodeHelpers.WriteErrorMessage($"Disposing Context", Listener, PodeLoggingLevel.Verbose, this); Listener.RemoveProcessingContext(this); if (IsClosed) { + PodeSocket.RemovePendingSocket(Socket); Request.Dispose(); Response.Dispose(); ContextTimeoutToken.Dispose(); @@ -401,7 +349,7 @@ public void Dispose(bool force) // are we awaiting for more info? if (IsHttp) { - _awaitingBody = (HttpRequest.AwaitingBody && !IsErrored && !IsTimeout); + _awaitingBody = HttpRequest.AwaitingBody && !IsErrored && !IsTimeout; } // only send a response if Http @@ -409,11 +357,11 @@ public void Dispose(bool force) { if (IsTimeout) { - Response.SendTimeout(); + Response.SendTimeout().Wait(); } else { - Response.Send(); + Response.Send().Wait(); } } @@ -430,7 +378,7 @@ public void Dispose(bool force) if (Response.SseEnabled) { - Response.CloseSseConnection(); + Response.CloseSseConnection().Wait(); } Request.Dispose(); @@ -446,8 +394,13 @@ public void Dispose(bool force) // if keep-alive, or awaiting body, setup for re-receive if ((_awaitingBody || (IsKeepAlive && !IsErrored && !IsTimeout && !Response.SseEnabled)) && !force) { + PodeHelpers.WriteErrorMessage($"Re-receiving Request", Listener, PodeLoggingLevel.Verbose, this); StartReceive(); } + else + { + PodeSocket.RemovePendingSocket(Socket); + } } } } diff --git a/src/Listener/PodeEndpoint.cs b/src/Listener/PodeEndpoint.cs index 917dfb643..ceaf167d3 100644 --- a/src/Listener/PodeEndpoint.cs +++ b/src/Listener/PodeEndpoint.cs @@ -1,6 +1,8 @@ using System; using System.Net; using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; namespace Pode { @@ -52,7 +54,7 @@ public void Listen() Socket.Listen(int.MaxValue); } - public bool AcceptAsync(SocketAsyncEventArgs args) + public bool Accept(SocketAsyncEventArgs args) { if (IsDisposed) { @@ -66,7 +68,7 @@ public void Dispose() { IsDisposed = true; PodeSocket.CloseSocket(Socket); - Socket = default(Socket); + Socket = default; } public new bool Equals(object obj) diff --git a/src/Listener/PodeFileWatcher.cs b/src/Listener/PodeFileWatcher.cs index da1249cdb..941b78be6 100644 --- a/src/Listener/PodeFileWatcher.cs +++ b/src/Listener/PodeFileWatcher.cs @@ -16,10 +16,12 @@ public PodeFileWatcher(string name, string path, bool includeSubdirectories, int { Name = name; - FileWatcher = new RecoveringFileSystemWatcher(path); - FileWatcher.IncludeSubdirectories = includeSubdirectories; - FileWatcher.InternalBufferSize = internalBufferSize; - FileWatcher.NotifyFilter = notifyFilters; + FileWatcher = new RecoveringFileSystemWatcher(path) + { + IncludeSubdirectories = includeSubdirectories, + InternalBufferSize = internalBufferSize, + NotifyFilter = notifyFilters + }; EventsRegistered = new HashSet(); RegisterEvent(PodeFileWatcherChangeType.Errored); diff --git a/src/Listener/PodeForm.cs b/src/Listener/PodeForm.cs index 5bfe7e170..b740e06c6 100644 --- a/src/Listener/PodeForm.cs +++ b/src/Listener/PodeForm.cs @@ -202,7 +202,7 @@ private static bool IsLineBoundary(byte[] bytes, string boundary, Encoding conte return false; } - return (contentEncoding.GetString(bytes).StartsWith(boundary)); + return contentEncoding.GetString(bytes).StartsWith(boundary); } public static bool IsLineBoundary(string line, string boundary) diff --git a/src/Listener/PodeFormData.cs b/src/Listener/PodeFormData.cs index 09e1dcf76..487fe8dda 100644 --- a/src/Listener/PodeFormData.cs +++ b/src/Listener/PodeFormData.cs @@ -11,15 +11,17 @@ public class PodeFormData public string[] Values => _values.ToArray(); public int Count => _values.Count; - public bool IsSingular => (_values.Count == 1); - public bool IsEmpty => (_values.Count == 0); + public bool IsSingular => _values.Count == 1; + public bool IsEmpty => _values.Count == 0; public PodeFormData(string key, string value) { Key = key; - _values = new List(); - _values.Add(value); + _values = new List + { + value + }; } public void AddValue(string value) diff --git a/src/Listener/PodeHelpers.cs b/src/Listener/PodeHelpers.cs index bb68f6742..44a7e8f95 100644 --- a/src/Listener/PodeHelpers.cs +++ b/src/Listener/PodeHelpers.cs @@ -5,6 +5,8 @@ using System.Security.Cryptography; using System.Reflection; using System.Runtime.Versioning; +using System.Threading.Tasks; +using System.Threading; namespace Pode { @@ -13,12 +15,14 @@ public class PodeHelpers public static readonly string[] HTTP_METHODS = new string[] { "CONNECT", "DELETE", "GET", "HEAD", "MERGE", "OPTIONS", "PATCH", "POST", "PUT", "TRACE" }; public const string WEB_SOCKET_MAGIC_KEY = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; public readonly static char[] NEW_LINE_ARRAY = new char[] { '\r', '\n' }; + public readonly static char[] SPACE_ARRAY = new char[] { ' ' }; public const string NEW_LINE = "\r\n"; public const string NEW_LINE_UNIX = "\n"; public const int BYTE_SIZE = sizeof(byte); public const byte NEW_LINE_BYTE = 10; public const byte CARRIAGE_RETURN_BYTE = 13; public const byte DASH_BYTE = 45; + public const byte PERIOD_BYTE = 46; private static string _dotnet_version = string.Empty; private static bool _is_net_framework = false; @@ -26,7 +30,7 @@ public static bool IsNetFramework { get { - if (String.IsNullOrWhiteSpace(_dotnet_version)) + if (string.IsNullOrWhiteSpace(_dotnet_version)) { _dotnet_version = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.FrameworkName ?? "Framework"; _is_net_framework = _dotnet_version.Equals("Framework", StringComparison.InvariantCultureIgnoreCase); @@ -71,7 +75,7 @@ public static bool IsNetFramework return true; } - PodeHelpers.WriteException(ex, connector, level); + WriteException(ex, connector, level); return false; }); } @@ -114,27 +118,50 @@ public static string NewGuid(int length = 16) { var bytes = new byte[length]; rnd.GetBytes(bytes); - return (new Guid(bytes)).ToString(); + return new Guid(bytes).ToString(); } } - public static void WriteTo(MemoryStream stream, byte[] array, int startIndex, int count = 0) + public static async Task WriteTo(MemoryStream stream, byte[] array, int startIndex, int count, CancellationToken cancellationToken) { + // Validate startIndex and count to avoid unnecessary work + if (startIndex < 0 || startIndex > array.Length) + { + throw new ArgumentOutOfRangeException(nameof(startIndex)); + } + if (count <= 0 || startIndex + count > array.Length) { count = array.Length - startIndex; } - stream.Write(array, startIndex, count); + // Perform the asynchronous write operation + if (count > 0) + { + await stream.WriteAsync(array, startIndex, count, cancellationToken).ConfigureAwait(false); + } } public static byte[] Slice(byte[] array, int startIndex, int count = 0) { + // Validate startIndex and adjust count if needed + if (startIndex < 0 || startIndex > array.Length) + { + throw new ArgumentOutOfRangeException(nameof(startIndex)); + } + + // If count is zero or less, or exceeds the array bounds, adjust it if (count <= 0 || startIndex + count > array.Length) { count = array.Length - startIndex; } + // If the count is zero, return an empty array + if (count == 0) + { + return Array.Empty(); + } + var newArray = new byte[count]; Buffer.BlockCopy(array, startIndex * BYTE_SIZE, newArray, 0, count * BYTE_SIZE); return newArray; diff --git a/src/Listener/PodeHttpRequest.cs b/src/Listener/PodeHttpRequest.cs index 54b6ffbd4..1f1f42875 100644 --- a/src/Listener/PodeHttpRequest.cs +++ b/src/Listener/PodeHttpRequest.cs @@ -5,10 +5,11 @@ using System.Net.Http; using System.Net.Sockets; using System.Text; -using System.Text.RegularExpressions; using System.Web; using System.Linq; using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace Pode { @@ -58,17 +59,17 @@ public string Body public override bool CloseImmediately { - get => (string.IsNullOrWhiteSpace(HttpMethod) - || (IsWebSocket && !HttpMethod.Equals("GET", StringComparison.InvariantCultureIgnoreCase))); + get => string.IsNullOrWhiteSpace(HttpMethod) + || (IsWebSocket && !HttpMethod.Equals("GET", StringComparison.InvariantCultureIgnoreCase)); } public override bool IsProcessable { - get => (!CloseImmediately && !AwaitingBody); + get => !CloseImmediately && !AwaitingBody; } - public PodeHttpRequest(Socket socket, PodeSocket podeSocket) - : base(socket, podeSocket) + public PodeHttpRequest(Socket socket, PodeSocket podeSocket, PodeContext context) + : base(socket, podeSocket, context) { Protocol = "HTTP/1.1"; Type = PodeProtocolType.Http; @@ -85,12 +86,11 @@ protected override bool ValidateInput(byte[] bytes) // wait until we have the rest of the payload if (AwaitingBody) { - return (bytes.Length >= (ContentLength - BodyStream.Length)); + return bytes.Length >= (ContentLength - BodyStream.Length); } - var lf = (byte)10; var previousIndex = -1; - var index = Array.IndexOf(bytes, lf); + var index = Array.IndexOf(bytes, PodeHelpers.NEW_LINE_BYTE); // do we have a request line yet? if (index == -1) @@ -102,7 +102,8 @@ protected override bool ValidateInput(byte[] bytes) if (!IsRequestLineValid) { var reqLine = Encoding.GetString(bytes, 0, index).Trim(); - var reqMeta = Regex.Split(reqLine, "\\s+"); + var reqMeta = reqLine.Split(PodeHelpers.SPACE_ARRAY, StringSplitOptions.RemoveEmptyEntries); + if (reqMeta.Length != 3) { throw new HttpRequestException($"Invalid request line: {reqLine} [{reqMeta.Length}]"); @@ -115,27 +116,17 @@ protected override bool ValidateInput(byte[] bytes) while (true) { previousIndex = index; - index = Array.IndexOf(bytes, lf, index + 1); - - if (index - previousIndex <= 2) - { - if (index - previousIndex == 1) - { - break; - } - - if (bytes[previousIndex + 1] == (byte)13) - { - break; - } - } + index = Array.IndexOf(bytes, PodeHelpers.NEW_LINE_BYTE, index + 1); - if (index == bytes.Length - 1) + // If the difference between indexes indicates the end of headers, exit the loop + if (index == previousIndex + 1 || + (index > previousIndex + 1 && bytes[previousIndex + 1] == PodeHelpers.CARRIAGE_RETURN_BYTE)) { break; } - if (index == -1) + // Return false if LF not found and end of array is reached + if (index == -1 || index >= bytes.Length - 1) { return false; } @@ -146,7 +137,7 @@ protected override bool ValidateInput(byte[] bytes) return true; } - protected override bool Parse(byte[] bytes) + protected override async Task Parse(byte[] bytes, CancellationToken cancellationToken) { // if there are no bytes, return (0 bytes read means we can close the socket) if (bytes.Length == 0) @@ -168,14 +159,14 @@ protected override bool Parse(byte[] bytes) var reqLines = content.Split(new string[] { newline }, StringSplitOptions.None); content = string.Empty; - bodyIndex = ParseHeaders(reqLines, newline); + bodyIndex = ParseHeaders(reqLines); bodyIndex = reqLines.Take(bodyIndex).Sum(x => x.Length) + (bodyIndex * newline.Length); - reqLines = default(string[]); + reqLines = default; } // parse the body - ParseBody(bytes, newline, bodyIndex); - AwaitingBody = (ContentLength > 0 && BodyStream.Length < ContentLength && Error == default(HttpRequestException)); + await ParseBody(bytes, newline, bodyIndex, cancellationToken).ConfigureAwait(false); + AwaitingBody = ContentLength > 0 && BodyStream.Length < ContentLength && Error == default(HttpRequestException); if (!AwaitingBody) { @@ -184,21 +175,21 @@ protected override bool Parse(byte[] bytes) if (BodyStream != default(MemoryStream)) { BodyStream.Dispose(); - BodyStream = default(MemoryStream); + BodyStream = default; } } - return (!AwaitingBody); + return !AwaitingBody; } - private int ParseHeaders(string[] reqLines, string newline) + private int ParseHeaders(string[] reqLines) { // reset raw body - RawBody = default(byte[]); + RawBody = default; _body = string.Empty; // first line is method/url - var reqMeta = Regex.Split(reqLines[0].Trim(), "\\s+"); + var reqMeta = reqLines[0].Trim().Split(' '); if (reqMeta.Length != 3) { throw new HttpRequestException($"Invalid request line: {reqLines[0]} [{reqMeta.Length}]"); @@ -206,18 +197,18 @@ private int ParseHeaders(string[] reqLines, string newline) // http method HttpMethod = reqMeta[0].Trim(); - if (Array.IndexOf(PodeHelpers.HTTP_METHODS, HttpMethod) == -1) + if (!PodeHelpers.HTTP_METHODS.Contains(HttpMethod)) { throw new HttpRequestException($"Invalid request HTTP method: {HttpMethod}"); } // query string var reqQuery = reqMeta[1].Trim(); - var qmIndex = string.IsNullOrEmpty(reqQuery) ? 0 : reqQuery.IndexOf("?"); + var qmIndex = reqQuery.IndexOf("?"); QueryString = qmIndex > 0 - ? HttpUtility.ParseQueryString(reqQuery.Substring(qmIndex)) - : default(NameValueCollection); + ? HttpUtility.ParseQueryString(reqQuery.Substring(qmIndex + 1)) + : default; // http protocol version Protocol = (reqMeta[2] ?? "HTTP/1.1").Trim(); @@ -226,7 +217,7 @@ private int ParseHeaders(string[] reqLines, string newline) throw new HttpRequestException($"Invalid request version: {Protocol}"); } - ProtocolVersion = Regex.Split(Protocol, "/")[1]; + ProtocolVersion = Protocol.Split('/')[1]; // headers Headers = new Hashtable(StringComparer.InvariantCultureIgnoreCase); @@ -246,38 +237,41 @@ private int ParseHeaders(string[] reqLines, string newline) } h_index = h_line.IndexOf(":"); - h_name = h_line.Substring(0, h_index).Trim(); - h_value = h_line.Substring(h_index + 1).Trim(); - Headers.Add(h_name, h_value); + if (h_index > 0) + { + h_name = h_line.Substring(0, h_index).Trim(); + h_value = h_line.Substring(h_index + 1).Trim(); + Headers.Add(h_name, h_value); + } } // build required URI details - var _proto = (IsSsl ? "https" : "http"); - Host = $"{Headers["Host"]}"; - Url = new Uri($"{_proto}://{Host}{reqQuery}"); + var _proto = IsSsl ? "https" : "http"; + Host = Headers["Host"]?.ToString(); // check the host header - if (!Context.PodeSocket.CheckHostname(Host)) + if (string.IsNullOrWhiteSpace(Host) || !Context.PodeSocket.CheckHostname(Host)) { - throw new HttpRequestException($"Invalid request Host: {Host}"); + throw new HttpRequestException($"Invalid Host header: {Host}"); } + // build the URL + Url = new Uri($"{_proto}://{Host}{reqQuery}"); + // get the content length - var strContentLength = $"{Headers["Content-Length"]}"; - if (string.IsNullOrWhiteSpace(strContentLength)) + ContentLength = 0; + if (int.TryParse(Headers["Content-Length"]?.ToString(), out int _contentLength)) { - strContentLength = "0"; + ContentLength = _contentLength; } - ContentLength = int.Parse(strContentLength); - // set the transfer encoding - TransferEncoding = $"{Headers["Transfer-Encoding"]}"; + TransferEncoding = Headers["Transfer-Encoding"]?.ToString(); // set other default headers - UrlReferrer = $"{Headers["Referer"]}"; - UserAgent = $"{Headers["User-Agent"]}"; - ContentType = $"{Headers["Content-Type"]}"; + UrlReferrer = Headers["Referer"]?.ToString(); + UserAgent = Headers["User-Agent"]?.ToString(); + ContentType = Headers["Content-Type"]?.ToString(); // set content encoding ContentEncoding = System.Text.Encoding.UTF8; @@ -286,9 +280,9 @@ private int ParseHeaders(string[] reqLines, string newline) var atoms = ContentType.Split(';'); foreach (var atom in atoms) { - if (atom.Trim().ToLowerInvariant().StartsWith("charset")) + if (atom.Trim().StartsWith("charset", StringComparison.InvariantCultureIgnoreCase)) { - ContentEncoding = System.Text.Encoding.GetEncoding((atom.Split('=')[1].Trim())); + ContentEncoding = System.Text.Encoding.GetEncoding(atom.Split('=')[1].Trim()); break; } } @@ -301,30 +295,32 @@ private int ParseHeaders(string[] reqLines, string newline) } // do we have an SSE ClientId? - SseClientId = $"{Headers["X-Pode-Sse-Client-Id"]}"; + SseClientId = Headers["X-Pode-Sse-Client-Id"]?.ToString(); if (HasSseClientId) { - SseClientName = $"{Headers["X-Pode-Sse-Name"]}"; - SseClientGroup = $"{Headers["X-Pode-Sse-Group"]}"; + SseClientName = Headers["X-Pode-Sse-Name"]?.ToString(); + SseClientGroup = Headers["X-Pode-Sse-Group"]?.ToString(); } // keep-alive? - IsKeepAlive = (IsWebSocket || + IsKeepAlive = IsWebSocket || (Headers.ContainsKey("Connection") - && $"{Headers["Connection"]}".Equals("keep-alive", StringComparison.InvariantCultureIgnoreCase))); + && Headers["Connection"]?.ToString().Equals("keep-alive", StringComparison.InvariantCultureIgnoreCase) == true); // return index where body starts in req return bodyIndex; } - private void ParseBody(byte[] bytes, string newline, int start) + private async Task ParseBody(byte[] bytes, string newline, int start, CancellationToken cancellationToken) { + // set the body stream if (BodyStream == default(MemoryStream)) { BodyStream = new MemoryStream(); } - var isChunked = (!string.IsNullOrWhiteSpace(TransferEncoding) && TransferEncoding.Contains("chunked")); + // are we chunked? + var isChunked = !string.IsNullOrWhiteSpace(TransferEncoding) && TransferEncoding.Contains("chunked"); // if chunked, and we have a content-length, fail if (isChunked && ContentLength > 0) @@ -346,12 +342,7 @@ private void ParseBody(byte[] bytes, string newline, int start) // get index of newline char, read start>index bytes as HEX for length c_index = Array.IndexOf(bytes, (byte)newline[0], start); c_hexBytes = PodeHelpers.Slice(bytes, start, c_index - start); - - c_hex = string.Empty; - foreach (var b in c_hexBytes) - { - c_hex += (char)b; - } + c_hex = Encoding.GetString(c_hexBytes.ToArray()); // if no length, continue c_length = Convert.ToInt32(c_hex, 16); @@ -368,19 +359,19 @@ private void ParseBody(byte[] bytes, string newline, int start) start = (start + c_length - 1) + newline.Length + 1; } - PodeHelpers.WriteTo(BodyStream, c_rawBytes.ToArray(), 0, c_rawBytes.Count); + await PodeHelpers.WriteTo(BodyStream, c_rawBytes.ToArray(), 0, c_rawBytes.Count, cancellationToken).ConfigureAwait(false); } // else use content length else if (ContentLength > 0) { - PodeHelpers.WriteTo(BodyStream, bytes, start, ContentLength); + await PodeHelpers.WriteTo(BodyStream, bytes, start, ContentLength, cancellationToken).ConfigureAwait(false); } // else just read all else { - PodeHelpers.WriteTo(BodyStream, bytes, start); + await PodeHelpers.WriteTo(BodyStream, bytes, start, 0, cancellationToken).ConfigureAwait(false); } // check body size @@ -398,9 +389,20 @@ public void ParseFormData() Form = PodeForm.Parse(RawBody, ContentType, ContentEncoding); } + public override void PartialDispose() + { + if (BodyStream != default(MemoryStream)) + { + BodyStream.Dispose(); + BodyStream = default; + } + + base.PartialDispose(); + } + public override void Dispose() { - RawBody = default(byte[]); + RawBody = default; _body = string.Empty; if (BodyStream != default(MemoryStream)) diff --git a/src/Listener/PodeListener.cs b/src/Listener/PodeListener.cs index f5a1dc655..64d8c1a06 100644 --- a/src/Listener/PodeListener.cs +++ b/src/Listener/PodeListener.cs @@ -46,7 +46,7 @@ public bool ShowServerDetails } } - public PodeListener(CancellationToken cancellationToken = default(CancellationToken)) + public PodeListener(CancellationToken cancellationToken = default) : base(cancellationToken) { Sockets = new List(); @@ -77,12 +77,12 @@ private void Bind(PodeSocket socket) Sockets.Add(socket); } - public PodeContext GetContext(CancellationToken cancellationToken = default(CancellationToken)) + public PodeContext GetContext(CancellationToken cancellationToken = default) { return Contexts.Get(cancellationToken); } - public Task GetContextAsync(CancellationToken cancellationToken = default(CancellationToken)) + public Task GetContextAsync(CancellationToken cancellationToken = default) { return Contexts.GetAsync(cancellationToken); } @@ -137,7 +137,7 @@ public void AddSseConnection(PodeServerEvent sse) public void SendSseEvent(string name, string[] groups, string[] clientIds, string eventType, string data, string id = null) { - Task.Factory.StartNew(() => + Task.Run(async () => { if (!ServerEvents.ContainsKey(name)) { @@ -158,7 +158,7 @@ public void SendSseEvent(string name, string[] groups, string[] clientIds, strin if (ServerEvents[name][clientId].IsForGroup(groups)) { - ServerEvents[name][clientId].Context.Response.SendSseEvent(eventType, data, id); + await ServerEvents[name][clientId].Context.Response.SendSseEvent(eventType, data, id).ConfigureAwait(false); } } }, CancellationToken); @@ -166,7 +166,7 @@ public void SendSseEvent(string name, string[] groups, string[] clientIds, strin public void CloseSseConnection(string name, string[] groups, string[] clientIds) { - Task.Factory.StartNew(() => + Task.Run(async () => { if (!ServerEvents.ContainsKey(name)) { @@ -187,7 +187,7 @@ public void CloseSseConnection(string name, string[] groups, string[] clientIds) if (ServerEvents[name][clientId].IsForGroup(groups)) { - ServerEvents[name][clientId].Context.Response.CloseSseConnection(); + await ServerEvents[name][clientId].Context.Response.CloseSseConnection().ConfigureAwait(false); } } }, CancellationToken); @@ -211,12 +211,12 @@ public bool TestSseConnectionExists(string name, string clientId) return true; } - public PodeServerSignal GetServerSignal(CancellationToken cancellationToken = default(CancellationToken)) + public PodeServerSignal GetServerSignal(CancellationToken cancellationToken = default) { return ServerSignals.Get(cancellationToken); } - public Task GetServerSignalAsync(CancellationToken cancellationToken = default(CancellationToken)) + public Task GetServerSignalAsync(CancellationToken cancellationToken = default) { return ServerSignals.GetAsync(cancellationToken); } @@ -231,12 +231,12 @@ public void RemoveProcessingServerSignal(PodeServerSignal signal) ServerSignals.RemoveProcessing(signal); } - public PodeClientSignal GetClientSignal(CancellationToken cancellationToken = default(CancellationToken)) + public PodeClientSignal GetClientSignal(CancellationToken cancellationToken = default) { return ClientSignals.Get(cancellationToken); } - public Task GetClientSignalAsync(CancellationToken cancellationToken = default(CancellationToken)) + public Task GetClientSignalAsync(CancellationToken cancellationToken = default) { return ClientSignals.GetAsync(cancellationToken); } diff --git a/src/Listener/PodeReceiver.cs b/src/Listener/PodeReceiver.cs index c723e2e2c..21cff8f2c 100644 --- a/src/Listener/PodeReceiver.cs +++ b/src/Listener/PodeReceiver.cs @@ -20,8 +20,10 @@ public class PodeReceiver : PodeConnector Start(); } - public void ConnectWebSocket(string name, string url, string contentType) + public async Task ConnectWebSocket(string name, string url, string contentType) { + var socket = default(PodeWebSocket); + lock (WebSockets) { if (WebSockets.ContainsKey(name)) @@ -29,16 +31,16 @@ public void ConnectWebSocket(string name, string url, string contentType) throw new Exception($"WebSocket connection with name {name} already defined"); } - var socket = new PodeWebSocket(name, url, contentType); - socket.BindReceiver(this); - socket.Connect(); + socket = new PodeWebSocket(name, url, contentType, this); WebSockets.Add(name, socket); } + + await socket.Connect().ConfigureAwait(false); } public PodeWebSocket GetWebSocket(string name) { - return (WebSockets.ContainsKey(name) ? WebSockets[name] : default(PodeWebSocket)); + return WebSockets.ContainsKey(name) ? WebSockets[name] : default; } public void DisconnectWebSocket(string name) diff --git a/src/Listener/PodeRequest.cs b/src/Listener/PodeRequest.cs index e0633e3a7..bc3bfc07e 100644 --- a/src/Listener/PodeRequest.cs +++ b/src/Listener/PodeRequest.cs @@ -30,20 +30,14 @@ public class PodeRequest : PodeProtocol, IDisposable public SslPolicyErrors ClientCertificateErrors { get; set; } public SslProtocols Protocols { get; private set; } public HttpRequestException Error { get; set; } - public bool IsAborted => (Error != default(HttpRequestException)); + public bool IsAborted => Error != default(HttpRequestException); public bool IsDisposed { get; private set; } - public virtual string Address - { - get => (Context.PodeSocket.HasHostnames + public virtual string Address => Context.PodeSocket.HasHostnames ? $"{Context.PodeSocket.Hostname}:{((IPEndPoint)LocalEndPoint).Port}" - : $"{((IPEndPoint)LocalEndPoint).Address}:{((IPEndPoint)LocalEndPoint).Port}"); - } + : $"{((IPEndPoint)LocalEndPoint).Address}:{((IPEndPoint)LocalEndPoint).Port}"; - public virtual string Scheme - { - get => (SslUpgraded ? $"{Context.PodeSocket.Type}s" : $"{Context.PodeSocket.Type}"); - } + public virtual string Scheme => SslUpgraded ? $"{Context.PodeSocket.Type}s" : $"{Context.PodeSocket.Type}"; private Socket Socket; protected PodeContext Context; @@ -53,16 +47,17 @@ public virtual string Scheme private MemoryStream BufferStream; private const int BufferSize = 16384; - public PodeRequest(Socket socket, PodeSocket podeSocket) + public PodeRequest(Socket socket, PodeSocket podeSocket, PodeContext context) { Socket = socket; RemoteEndPoint = socket.RemoteEndPoint; LocalEndPoint = socket.LocalEndPoint; TlsMode = podeSocket.TlsMode; Certificate = podeSocket.Certificate; - IsSsl = (Certificate != default(X509Certificate)); + IsSsl = Certificate != default(X509Certificate); AllowClientCertificate = podeSocket.AllowClientCertificate; Protocols = podeSocket.Protocols; + Context = context; } public PodeRequest(PodeRequest request) @@ -81,7 +76,7 @@ public PodeRequest(PodeRequest request) TlsMode = request.TlsMode; } - public void Open() + public async Task Open(CancellationToken cancellationToken) { // open the socket's stream InputStream = new NetworkStream(Socket, true); @@ -92,20 +87,41 @@ public void Open() } // otherwise, convert the stream to an ssl stream - UpgradeToSSL(); + await UpgradeToSSL(cancellationToken).ConfigureAwait(false); } - public void UpgradeToSSL() + public async Task UpgradeToSSL(CancellationToken cancellationToken) { + // if we've already upgraded, return if (SslUpgraded) { return; } + // create the ssl stream var ssl = new SslStream(InputStream, false, new RemoteCertificateValidationCallback(ValidateCertificateCallback)); - ssl.AuthenticateAsServerAsync(Certificate, AllowClientCertificate, Protocols, false).Wait(Context.Listener.CancellationToken); - InputStream = ssl; - SslUpgraded = true; + + using (cancellationToken.Register(() => ssl.Dispose())) + { + try + { + // authenticate the stream + await ssl.AuthenticateAsServerAsync(Certificate, AllowClientCertificate, Protocols, false).ConfigureAwait(false); + + // if we've upgraded, set the stream + InputStream = ssl; + SslUpgraded = true; + } + catch (OperationCanceledException) { } + catch (IOException) { } + catch (ObjectDisposedException) { } + catch (Exception ex) + { + PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Error); + Error = new HttpRequestException(ex.Message, ex); + Error.Data.Add("PodeStatusCode", 502); + } + } } private bool ValidateCertificateCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) @@ -113,69 +129,71 @@ private bool ValidateCertificateCallback(object sender, X509Certificate certific ClientCertificateErrors = sslPolicyErrors; ClientCertificate = certificate == default(X509Certificate) - ? default(X509Certificate2) + ? default : new X509Certificate2(certificate); return true; } - protected async Task BeginRead(byte[] buffer, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - return await Task.Factory.FromAsync(InputStream.BeginRead, InputStream.EndRead, buffer, 0, BufferSize, null); - } - public async Task Receive(CancellationToken cancellationToken) { try { - Error = default(HttpRequestException); + Error = default; Buffer = new byte[BufferSize]; - BufferStream = new MemoryStream(); - - var read = 0; - var close = true; - - while ((read = await BeginRead(Buffer, cancellationToken)) > 0) + using (BufferStream = new MemoryStream()) { - cancellationToken.ThrowIfCancellationRequested(); - BufferStream.Write(Buffer, 0, read); + var close = true; - if (Socket.Available > 0 || !ValidateInput(BufferStream.ToArray())) + while (true) { - continue; - } - - if (!Parse(BufferStream.ToArray())) - { - BufferStream.Dispose(); - BufferStream = new MemoryStream(); - continue; + // read the input stream + var read = await InputStream.ReadAsync(Buffer, 0, BufferSize, cancellationToken).ConfigureAwait(false); + if (read <= 0) + { + break; + } + + // write the buffer to the stream + await BufferStream.WriteAsync(Buffer, 0, read, cancellationToken).ConfigureAwait(false); + + // if we have more data, or the input is invalid, continue + if (Socket.Available > 0 || !ValidateInput(BufferStream.ToArray())) + { + continue; + } + + // parse the buffer + if (!await Parse(BufferStream.ToArray(), cancellationToken).ConfigureAwait(false)) + { + BufferStream.SetLength(0); + continue; + } + + close = false; + break; } - close = false; - break; + return close; } - - cancellationToken.ThrowIfCancellationRequested(); - return close; } + catch (OperationCanceledException) { } + catch (IOException) { } catch (HttpRequestException httpex) { + PodeHelpers.WriteException(httpex, Context.Listener, PodeLoggingLevel.Error); Error = httpex; } catch (Exception ex) { - cancellationToken.ThrowIfCancellationRequested(); + PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Error); Error = new HttpRequestException(ex.Message, ex); Error.Data.Add("PodeStatusCode", 400); } finally { - BufferStream.Dispose(); - BufferStream = default(MemoryStream); - Buffer = default(byte[]); + PartialDispose(); } return false; @@ -184,16 +202,21 @@ public async Task Receive(CancellationToken cancellationToken) public async Task Read(byte[] checkBytes, CancellationToken cancellationToken) { var buffer = new byte[BufferSize]; - var bufferStream = new MemoryStream(); - - try + using (var bufferStream = new MemoryStream()) { - var read = 0; - while ((read = await BeginRead(buffer, cancellationToken)) > 0) + while (true) { - cancellationToken.ThrowIfCancellationRequested(); - bufferStream.Write(buffer, 0, read); + // read the input stream + var read = await InputStream.ReadAsync(buffer, 0, BufferSize, cancellationToken).ConfigureAwait(false); + if (read <= 0) + { + break; + } + + // write the buffer to the stream + await bufferStream.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false); + // if we have more data, or the input is invalid, continue if (Socket.Available > 0 || !ValidateInputInternal(bufferStream.ToArray(), checkBytes)) { continue; @@ -202,15 +225,8 @@ public async Task Read(byte[] checkBytes, CancellationToken cancellation break; } - cancellationToken.ThrowIfCancellationRequested(); return Encoding.GetString(bufferStream.ToArray()).Trim(); } - finally - { - bufferStream.Dispose(); - bufferStream = default(MemoryStream); - buffer = default(byte[]); - } } private bool ValidateInputInternal(byte[] bytes, byte[] checkBytes) @@ -245,7 +261,7 @@ private bool ValidateInputInternal(byte[] bytes, byte[] checkBytes) return true; } - protected virtual bool Parse(byte[] bytes) + protected virtual Task Parse(byte[] bytes, CancellationToken cancellationToken) { throw new NotImplementedException(); } @@ -255,9 +271,15 @@ protected virtual bool ValidateInput(byte[] bytes) return true; } - public void SetContext(PodeContext context) + public virtual void PartialDispose() { - Context = context; + if (BufferStream != default(MemoryStream)) + { + BufferStream.Dispose(); + BufferStream = default; + } + + Buffer = default; } public virtual void Dispose() @@ -277,15 +299,10 @@ public virtual void Dispose() if (InputStream != default(Stream)) { InputStream.Dispose(); - InputStream = default(Stream); - } - - if (BufferStream != default(MemoryStream)) - { - BufferStream.Dispose(); - BufferStream = default(MemoryStream); + InputStream = default; } + PartialDispose(); PodeHelpers.WriteErrorMessage($"Request disposed", Context.Listener, PodeLoggingLevel.Verbose, Context); } } diff --git a/src/Listener/PodeResponse.cs b/src/Listener/PodeResponse.cs index 3c42a9865..786545994 100644 --- a/src/Listener/PodeResponse.cs +++ b/src/Listener/PodeResponse.cs @@ -5,6 +5,7 @@ using System.IO; using System.Net; using System.Text; +using System.Threading.Tasks; namespace Pode { @@ -77,13 +78,14 @@ public string HttpResponseLine private static UTF8Encoding Encoding = new UTF8Encoding(); - public PodeResponse() + public PodeResponse(PodeContext context) { Headers = new PodeResponseHeaders(); OutputStream = new MemoryStream(); + Context = context; } - public void Send() + public async Task Send() { if (Sent || IsDisposed || (SentHeaders && SseEnabled)) { @@ -94,12 +96,12 @@ public void Send() try { - SendHeaders(Context.IsTimeout); - SendBody(Context.IsTimeout); + await SendHeaders(Context.IsTimeout).ConfigureAwait(false); + await SendBody(Context.IsTimeout).ConfigureAwait(false); PodeHelpers.WriteErrorMessage($"Response sent", Context.Listener, PodeLoggingLevel.Verbose, Context); } - catch (OperationCanceledException) {} - catch (IOException) {} + catch (OperationCanceledException) { } + catch (IOException) { } catch (AggregateException aex) { PodeHelpers.HandleAggregateException(aex, Context.Listener); @@ -111,11 +113,11 @@ public void Send() } finally { - Flush(); + await Flush().ConfigureAwait(false); } } - public void SendTimeout() + public async Task SendTimeout() { if (SentHeaders || IsDisposed) { @@ -127,11 +129,11 @@ public void SendTimeout() try { - SendHeaders(true); + await SendHeaders(true).ConfigureAwait(false); PodeHelpers.WriteErrorMessage($"Response timed-out sent", Context.Listener, PodeLoggingLevel.Verbose, Context); } - catch (OperationCanceledException) {} - catch (IOException) {} + catch (OperationCanceledException) { } + catch (IOException) { } catch (AggregateException aex) { PodeHelpers.HandleAggregateException(aex, Context.Listener); @@ -143,11 +145,11 @@ public void SendTimeout() } finally { - Flush(); + await Flush().ConfigureAwait(false); } } - private void SendHeaders(bool timeout) + private async Task SendHeaders(bool timeout) { if (SentHeaders || !Request.InputStream.CanWrite) { @@ -164,12 +166,12 @@ private void SendHeaders(bool timeout) // stream response output var buffer = Encoding.GetBytes(BuildHeaders(Headers)); - Request.InputStream.WriteAsync(buffer, 0, buffer.Length).Wait(Context.Listener.CancellationToken); - buffer = default(byte[]); + await Request.InputStream.WriteAsync(buffer, 0, buffer.Length, Context.Listener.CancellationToken).ConfigureAwait(false); + buffer = default; SentHeaders = true; } - private void SendBody(bool timeout) + private async Task SendBody(bool timeout) { if (SentBody || SseEnabled || !Request.InputStream.CanWrite) { @@ -179,21 +181,21 @@ private void SendBody(bool timeout) // stream response output if (!timeout && OutputStream.Length > 0) { - OutputStream.WriteTo(Request.InputStream); + await Task.Run(() => OutputStream.WriteTo(Request.InputStream), Context.Listener.CancellationToken).ConfigureAwait(false); } SentBody = true; } - public void Flush() + public async Task Flush() { if (Request.InputStream.CanWrite) { - Request.InputStream.Flush(); + await Request.InputStream.FlushAsync().ConfigureAwait(false); } } - public string SetSseConnection(PodeSseScope scope, string clientId, string name, string group, int retry, bool allowAllOrigins) + public async Task SetSseConnection(PodeSseScope scope, string clientId, string name, string group, int retry, bool allowAllOrigins) { // do nothing for no scope if (scope == PodeSseScope.None) @@ -231,9 +233,9 @@ public string SetSseConnection(PodeSseScope scope, string clientId, string name, } // send headers, and open event - Send(); - SendSseRetry(retry); - SendSseEvent("pode.open", $"{{\"clientId\":\"{clientId}\",\"group\":\"{group}\",\"name\":\"{name}\"}}"); + await Send().ConfigureAwait(false); + await SendSseRetry(retry).ConfigureAwait(false); + await SendSseEvent("pode.open", $"{{\"clientId\":\"{clientId}\",\"group\":\"{group}\",\"name\":\"{name}\"}}").ConfigureAwait(false); // if global, cache connection in listener if (scope == PodeSseScope.Global) @@ -245,60 +247,60 @@ public string SetSseConnection(PodeSseScope scope, string clientId, string name, return clientId; } - public void CloseSseConnection() + public async Task CloseSseConnection() { - SendSseEvent("pode.close", string.Empty); + await SendSseEvent("pode.close", string.Empty).ConfigureAwait(false); } - public void SendSseEvent(string eventType, string data, string id = null) + public async Task SendSseEvent(string eventType, string data, string id = null) { if (!string.IsNullOrEmpty(id)) { - WriteLine($"id: {id}"); + await WriteLine($"id: {id}").ConfigureAwait(false); } if (!string.IsNullOrEmpty(eventType)) { - WriteLine($"event: {eventType}"); + await WriteLine($"event: {eventType}").ConfigureAwait(false); } - WriteLine($"data: {data}{PodeHelpers.NEW_LINE}", true); + await WriteLine($"data: {data}{PodeHelpers.NEW_LINE}", true).ConfigureAwait(false); } - public void SendSseRetry(int retry) + public async Task SendSseRetry(int retry) { if (retry <= 0) { return; } - WriteLine($"retry: {retry}", true); + await WriteLine($"retry: {retry}", true).ConfigureAwait(false); } - public void SendSignal(PodeServerSignal signal) + public async Task SendSignal(PodeServerSignal signal) { if (!string.IsNullOrEmpty(signal.Value)) { - Write(signal.Value); + await Write(signal.Value).ConfigureAwait(false); } } - public void Write(string message, bool flush = false) + public async Task Write(string message, bool flush = false) { // simple messages if (!Context.IsWebSocket) { - Write(Encoding.GetBytes(message), flush); + await Write(Encoding.GetBytes(message), flush).ConfigureAwait(false); } // web socket message else { - WriteFrame(message, PodeWsOpCode.Text, flush); + await WriteFrame(message, PodeWsOpCode.Text, flush).ConfigureAwait(false); } } - public void WriteFrame(string message, PodeWsOpCode opCode = PodeWsOpCode.Text, bool flush = false) + public async Task WriteFrame(string message, PodeWsOpCode opCode = PodeWsOpCode.Text, bool flush = false) { if (IsDisposed) { @@ -332,15 +334,15 @@ public void WriteFrame(string message, PodeWsOpCode opCode = PodeWsOpCode.Text, } buffer.AddRange(msgBytes); - Write(buffer.ToArray(), flush); + await Write(buffer.ToArray(), flush).ConfigureAwait(false); } - public void WriteLine(string message, bool flush = false) + public async Task WriteLine(string message, bool flush = false) { - Write(Encoding.GetBytes($"{message}{PodeHelpers.NEW_LINE}"), flush); + await Write(Encoding.GetBytes($"{message}{PodeHelpers.NEW_LINE}"), flush).ConfigureAwait(false); } - public void Write(byte[] buffer, bool flush = false) + public async Task Write(byte[] buffer, bool flush = false) { if (Request.IsDisposed || !Request.InputStream.CanWrite) { @@ -349,15 +351,15 @@ public void Write(byte[] buffer, bool flush = false) try { - Request.InputStream.WriteAsync(buffer, 0, buffer.Length).Wait(Context.Listener.CancellationToken); + await Request.InputStream.WriteAsync(buffer, 0, buffer.Length, Context.Listener.CancellationToken).ConfigureAwait(false); if (flush) { - Flush(); + await Flush().ConfigureAwait(false); } } - catch (OperationCanceledException) {} - catch (IOException) {} + catch (OperationCanceledException) { } + catch (IOException) { } catch (AggregateException aex) { PodeHelpers.HandleAggregateException(aex, Context.Listener); @@ -445,11 +447,6 @@ private string BuildHeaders(PodeResponseHeaders headers) return builder.ToString(); } - public void SetContext(PodeContext context) - { - Context = context; - } - public void Dispose() { if (IsDisposed) @@ -462,7 +459,7 @@ public void Dispose() if (OutputStream != default(MemoryStream)) { OutputStream.Dispose(); - OutputStream = default(MemoryStream); + OutputStream = default; } PodeHelpers.WriteErrorMessage($"Response disposed", Context.Listener, PodeLoggingLevel.Verbose, Context); diff --git a/src/Listener/PodeSignalRequest.cs b/src/Listener/PodeSignalRequest.cs index 903a7f35e..f7acc49cb 100644 --- a/src/Listener/PodeSignalRequest.cs +++ b/src/Listener/PodeSignalRequest.cs @@ -1,5 +1,7 @@ using System; using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; namespace Pode { @@ -27,12 +29,12 @@ public string CloseDescription public override bool CloseImmediately { - get => (OpCode == PodeWsOpCode.Close); + get => OpCode == PodeWsOpCode.Close; } public override bool IsProcessable { - get => (!CloseImmediately && OpCode != PodeWsOpCode.Pong && OpCode != PodeWsOpCode.Ping && !string.IsNullOrEmpty(Body)); + get => !CloseImmediately && OpCode != PodeWsOpCode.Pong && OpCode != PodeWsOpCode.Ping && !string.IsNullOrEmpty(Body); } public PodeSignalRequest(PodeHttpRequest request, PodeSignal signal) @@ -42,7 +44,7 @@ public PodeSignalRequest(PodeHttpRequest request, PodeSignal signal) IsKeepAlive = true; Type = PodeProtocolType.Ws; - var _proto = (IsSsl ? "wss" : "ws"); + var _proto = IsSsl ? "wss" : "ws"; Host = request.Host; Url = new Uri($"{_proto}://{request.Url.Authority}{request.Url.PathAndQuery}"); } @@ -52,7 +54,7 @@ public PodeClientSignal NewClientSignal() return new PodeClientSignal(Signal, Body, Context.Listener); } - protected override bool Parse(byte[] bytes) + protected override async Task Parse(byte[] bytes, CancellationToken cancellationToken) { // get the length and op-code var dataLength = bytes[1] - 128; @@ -118,7 +120,7 @@ protected override bool Parse(byte[] bytes) // send back a pong case PodeWsOpCode.Ping: - Context.Response.WriteFrame(string.Empty, PodeWsOpCode.Pong); + await Context.Response.WriteFrame(string.Empty, PodeWsOpCode.Pong).ConfigureAwait(false); break; } @@ -131,7 +133,7 @@ public override void Dispose() if (!IsDisposed) { PodeHelpers.WriteErrorMessage($"Closing Websocket", Context.Listener, PodeLoggingLevel.Verbose, Context); - Context.Response.WriteFrame(string.Empty, PodeWsOpCode.Close); + Context.Response.WriteFrame(string.Empty, PodeWsOpCode.Close).Wait(); } // remove client, and dispose diff --git a/src/Listener/PodeSmtpRequest.cs b/src/Listener/PodeSmtpRequest.cs index 8039d45f3..6bb1290d5 100644 --- a/src/Listener/PodeSmtpRequest.cs +++ b/src/Listener/PodeSmtpRequest.cs @@ -8,6 +8,8 @@ using System.Globalization; using _Encoding = System.Text.Encoding; using System.IO; +using System.Threading.Tasks; +using System.Threading; namespace Pode { @@ -29,17 +31,17 @@ public class PodeSmtpRequest : PodeRequest public override bool CloseImmediately { - get => (Command == PodeSmtpCommand.None || Command == PodeSmtpCommand.Quit); + get => Command == PodeSmtpCommand.None || Command == PodeSmtpCommand.Quit; } private bool _canProcess = false; public override bool IsProcessable { - get => (!CloseImmediately && _canProcess); + get => !CloseImmediately && _canProcess; } - public PodeSmtpRequest(Socket socket, PodeSocket podeSocket) - : base(socket, podeSocket) + public PodeSmtpRequest(Socket socket, PodeSocket podeSocket, PodeContext context) + : base(socket, podeSocket, context) { _canProcess = false; IsKeepAlive = true; @@ -58,13 +60,13 @@ private bool IsCommand(string content, string command) return content.StartsWith(command, true, CultureInfo.InvariantCulture); } - public void SendAck() + public async Task SendAck() { var ack = string.IsNullOrWhiteSpace(Context.PodeSocket.AcknowledgeMessage) ? $"{Context.PodeSocket.Hostname} -- Pode Proxy Server" : Context.PodeSocket.AcknowledgeMessage; - Context.Response.WriteLine($"220 {ack}", true); + await Context.Response.WriteLine($"220 {ack}", true).ConfigureAwait(false); } protected override bool ValidateInput(byte[] bytes) @@ -83,15 +85,15 @@ protected override bool ValidateInput(byte[] bytes) return false; } - return (bytes[bytes.Length - 3] == (byte)46 - && bytes[bytes.Length - 2] == (byte)13 - && bytes[bytes.Length - 1] == (byte)10); + return bytes[bytes.Length - 3] == PodeHelpers.PERIOD_BYTE + && bytes[bytes.Length - 2] == PodeHelpers.CARRIAGE_RETURN_BYTE + && bytes[bytes.Length - 1] == PodeHelpers.NEW_LINE_BYTE; } return true; } - protected override bool Parse(byte[] bytes) + protected override async Task Parse(byte[] bytes, CancellationToken cancellationToken) { // if there are no bytes, return (0 bytes read means we can close the socket) if (bytes.Length == 0) @@ -107,7 +109,7 @@ protected override bool Parse(byte[] bytes) if (string.IsNullOrWhiteSpace(content)) { Command = PodeSmtpCommand.None; - Context.Response.WriteLine("501 Invalid command received", true); + await Context.Response.WriteLine("501 Invalid command received", true).ConfigureAwait(false); return true; } @@ -115,7 +117,7 @@ protected override bool Parse(byte[] bytes) if (IsCommand(content, "QUIT")) { Command = PodeSmtpCommand.Quit; - Context.Response.WriteLine("221 OK", true); + await Context.Response.WriteLine("221 OK", true).ConfigureAwait(false); return true; } @@ -123,7 +125,7 @@ protected override bool Parse(byte[] bytes) if (StartType == PodeSmtpStartType.Ehlo && TlsMode == PodeTlsMode.Explicit && !SslUpgraded && !IsCommand(content, "STARTTLS")) { Command = PodeSmtpCommand.None; - Context.Response.WriteLine("530 Must issue a STARTTLS command first", true); + await Context.Response.WriteLine("530 Must issue a STARTTLS command first", true).ConfigureAwait(false); return true; } @@ -132,7 +134,7 @@ protected override bool Parse(byte[] bytes) { Command = PodeSmtpCommand.Helo; StartType = PodeSmtpStartType.Helo; - Context.Response.WriteLine("250 OK", true); + await Context.Response.WriteLine("250 OK", true).ConfigureAwait(false); return true; } @@ -141,14 +143,14 @@ protected override bool Parse(byte[] bytes) { Command = PodeSmtpCommand.Ehlo; StartType = PodeSmtpStartType.Ehlo; - Context.Response.WriteLine($"250-{Context.PodeSocket.Hostname} hello there", true); + await Context.Response.WriteLine($"250-{Context.PodeSocket.Hostname} hello there", true).ConfigureAwait(false); if (TlsMode == PodeTlsMode.Explicit && !SslUpgraded) { - Context.Response.WriteLine("250-STARTTLS", true); + await Context.Response.WriteLine("250-STARTTLS", true).ConfigureAwait(false); } - Context.Response.WriteLine("250 OK", true); + await Context.Response.WriteLine("250 OK", true).ConfigureAwait(false); return true; } @@ -158,14 +160,14 @@ protected override bool Parse(byte[] bytes) if (TlsMode != PodeTlsMode.Explicit) { Command = PodeSmtpCommand.None; - Context.Response.WriteLine("501 SMTP server not running on Explicit TLS for the STARTTLS command", true); + await Context.Response.WriteLine("501 SMTP server not running on Explicit TLS for the STARTTLS command", true).ConfigureAwait(false); return true; } Reset(); Command = PodeSmtpCommand.StartTls; - Context.Response.WriteLine("220 Ready to start TLS"); - UpgradeToSSL(); + await Context.Response.WriteLine("220 Ready to start TLS").ConfigureAwait(false); + await UpgradeToSSL(cancellationToken).ConfigureAwait(false); return true; } @@ -174,7 +176,7 @@ protected override bool Parse(byte[] bytes) { Reset(); Command = PodeSmtpCommand.Reset; - Context.Response.WriteLine("250 OK", true); + await Context.Response.WriteLine("250 OK", true).ConfigureAwait(false); return true; } @@ -182,7 +184,7 @@ protected override bool Parse(byte[] bytes) if (IsCommand(content, "NOOP")) { Command = PodeSmtpCommand.NoOp; - Context.Response.WriteLine("250 OK", true); + await Context.Response.WriteLine("250 OK", true).ConfigureAwait(false); return true; } @@ -190,7 +192,7 @@ protected override bool Parse(byte[] bytes) if (IsCommand(content, "RCPT TO")) { Command = PodeSmtpCommand.RcptTo; - Context.Response.WriteLine("250 OK", true); + await Context.Response.WriteLine("250 OK", true).ConfigureAwait(false); To.Add(ParseEmail(content)); return true; } @@ -199,7 +201,7 @@ protected override bool Parse(byte[] bytes) if (IsCommand(content, "MAIL FROM")) { Command = PodeSmtpCommand.MailFrom; - Context.Response.WriteLine("250 OK", true); + await Context.Response.WriteLine("250 OK", true).ConfigureAwait(false); From = ParseEmail(content); return true; } @@ -208,7 +210,7 @@ protected override bool Parse(byte[] bytes) if (IsCommand(content, "DATA")) { Command = PodeSmtpCommand.Data; - Context.Response.WriteLine("354 Start mail input; end with .", true); + await Context.Response.WriteLine("354 Start mail input; end with .", true).ConfigureAwait(false); return true; } @@ -217,17 +219,17 @@ protected override bool Parse(byte[] bytes) { case PodeSmtpCommand.Data: _canProcess = true; - Context.Response.WriteLine("250 OK", true); + await Context.Response.WriteLine("250 OK", true).ConfigureAwait(false); RawBody = bytes; Attachments = new List(); // parse the headers Headers = ParseHeaders(content); - Subject = $"{Headers["Subject"]}"; - IsUrgent = ($"{Headers["Priority"]}".Equals("urgent", StringComparison.InvariantCultureIgnoreCase) || $"{Headers["Importance"]}".Equals("high", StringComparison.InvariantCultureIgnoreCase)); - ContentEncoding = $"{Headers["Content-Transfer-Encoding"]}"; + Subject = Headers["Subject"]?.ToString(); + IsUrgent = $"{Headers["Priority"]}".Equals("urgent", StringComparison.InvariantCultureIgnoreCase) || $"{Headers["Importance"]}".Equals("high", StringComparison.InvariantCultureIgnoreCase); + ContentEncoding = Headers["Content-Transfer-Encoding"]?.ToString(); - ContentType = $"{Headers["Content-Type"]}"; + ContentType = Headers["Content-Type"]?.ToString(); if (!string.IsNullOrEmpty(Boundary) && !ContentType.Contains("boundary=")) { ContentType = ContentType.TrimEnd(';'); @@ -249,7 +251,7 @@ protected override bool Parse(byte[] bytes) else { Command = PodeSmtpCommand.None; - Context.Response.WriteLine("501 Invalid DATA received", true); + await Context.Response.WriteLine("501 Invalid DATA received", true).ConfigureAwait(false); return true; } break; @@ -270,7 +272,7 @@ public void Reset() From = string.Empty; To = new List(); Body = string.Empty; - RawBody = default(byte[]); + RawBody = default; Command = PodeSmtpCommand.None; ContentType = string.Empty; ContentEncoding = string.Empty; @@ -365,7 +367,7 @@ private Hashtable ParseHeaders(string value) private bool IsBodyValid(string value) { var lines = value.Split(new string[] { PodeHelpers.NEW_LINE }, StringSplitOptions.None); - return (Array.LastIndexOf(lines, ".") > -1); + return Array.LastIndexOf(lines, ".") > -1; } private void ParseBoundary() @@ -464,7 +466,7 @@ private byte[] ConvertBodyEncoding(string body, string contentEncoding) var match = default(Match); while ((match = Regex.Match(body, "(?=(?[0-9A-F]{2}))")).Success) { - body = (body.Replace(match.Groups["code"].Value, $"{(char)Convert.ToInt32(match.Groups["hex"].Value, 16)}")); + body = body.Replace(match.Groups["code"].Value, $"{(char)Convert.ToInt32(match.Groups["hex"].Value, 16)}"); } return _Encoding.UTF8.GetBytes(body); @@ -516,7 +518,7 @@ private string ConvertBodyType(byte[] bytes, string contentType) public override void Dispose() { - RawBody = default(byte[]); + RawBody = default; Body = string.Empty; if (Attachments != default(List)) diff --git a/src/Listener/PodeSocket.cs b/src/Listener/PodeSocket.cs index 8f8b5fd7d..7c337c9e3 100644 --- a/src/Listener/PodeSocket.cs +++ b/src/Listener/PodeSocket.cs @@ -25,15 +25,11 @@ public class PodeSocket : PodeProtocol, IDisposable public bool DualMode { get; private set; } private ConcurrentQueue AcceptConnections; - private ConcurrentQueue ReceiveConnections; private IDictionary PendingSockets; private PodeListener Listener; - public bool IsSsl - { - get => Certificate != default(X509Certificate); - } + public bool IsSsl => Certificate != default(X509Certificate); private int _receiveTimeout; public int ReceiveTimeout @@ -50,11 +46,7 @@ public int ReceiveTimeout } public bool HasHostnames => Hostnames.Any(); - - public string Hostname - { - get => HasHostnames ? Hostnames[0] : Endpoints[0].IPAddress.ToString(); - } + public string Hostname => HasHostnames ? Hostnames[0] : Endpoints[0].IPAddress.ToString(); public PodeSocket(string name, IPAddress[] ipAddress, int port, SslProtocols protocols, PodeProtocolType type, X509Certificate certificate = null, bool allowClientCertificate = false, PodeTlsMode tlsMode = PodeTlsMode.Implicit, bool dualMode = false) : base(type) @@ -68,7 +60,6 @@ public PodeSocket(string name, IPAddress[] ipAddress, int port, SslProtocols pro DualMode = dualMode; AcceptConnections = new ConcurrentQueue(); - ReceiveConnections = new ConcurrentQueue(); PendingSockets = new Dictionary(); Endpoints = new List(); @@ -95,13 +86,13 @@ public void Start() { foreach (var ep in Endpoints) { - StartEndpoint(ep); + _ = Task.Run(() => StartEndpoint(ep), Listener.CancellationToken); } } private void StartEndpoint(PodeEndpoint endpoint) { - if (endpoint.IsDisposed) + if (endpoint.IsDisposed || Listener.CancellationToken.IsCancellationRequested) { return; } @@ -117,7 +108,7 @@ private void StartEndpoint(PodeEndpoint endpoint) try { - raised = endpoint.AcceptAsync(args); + raised = endpoint.Accept(args); } catch (ObjectDisposedException) { @@ -130,49 +121,46 @@ private void StartEndpoint(PodeEndpoint endpoint) } } - private void StartReceive(Socket acceptedSocket) + private async Task StartReceive(Socket acceptedSocket) { + // add the socket to pending + AddPendingSocket(acceptedSocket); + + // create the context var context = new PodeContext(acceptedSocket, this, Listener); + PodeHelpers.WriteErrorMessage($"Opening Receive", Listener, PodeLoggingLevel.Verbose, context); + + // initialise the context + await context.Initialise().ConfigureAwait(false); if (context.IsErrored) { context.Dispose(true); return; } + // start receiving data StartReceive(context); } public void StartReceive(PodeContext context) { - var args = GetReceiveConnection(); - args.AcceptSocket = context.Socket; - args.UserToken = context; - StartReceive(args); - } - - private void StartReceive(SocketAsyncEventArgs args) - { - args.SetBuffer(new byte[0], 0, 0); - bool raised; + PodeHelpers.WriteErrorMessage($"Starting Receive", Listener, PodeLoggingLevel.Verbose, context); try { - AddPendingSocket(args.AcceptSocket); - raised = args.AcceptSocket.ReceiveAsync(args); + _ = Task.Run(async () => await context.Receive().ConfigureAwait(false), Listener.CancellationToken); } - catch (ObjectDisposedException) + catch (OperationCanceledException) { } + catch (IOException) { } + catch (AggregateException aex) { - return; + PodeHelpers.HandleAggregateException(aex, Listener, PodeLoggingLevel.Error, true); + context.Socket.Close(); } catch (Exception ex) { PodeHelpers.WriteException(ex, Listener); - throw; - } - - if (!raised) - { - ProcessReceive(args); + context.Socket.Close(); } } @@ -205,69 +193,28 @@ private void ProcessAccept(SocketAsyncEventArgs args) else { // start receive - StartReceive(args.AcceptSocket); - } - - // add args back to connections - ClearSocketAsyncEvent(args); - AcceptConnections.Enqueue(args); - } - - private void ProcessReceive(SocketAsyncEventArgs args) - { - // get details - var received = args.AcceptSocket; - var context = (PodeContext)args.UserToken; - var error = args.SocketError; - - // remove the socket from pending - RemovePendingSocket(received); - - // close socket if not successful, or if listener is stopped - close now! - if ((received == default(Socket)) || (error != SocketError.Success) || (!Listener.IsConnected)) - { - if (error != SocketError.Success) + try { - PodeHelpers.WriteErrorMessage($"Closing receiving socket: {error}", Listener, PodeLoggingLevel.Debug); + _ = Task.Run(async () => await StartReceive(accepted), Listener.CancellationToken).ConfigureAwait(false); } - - // close socket - if (received != default(Socket)) + catch (OperationCanceledException) { } + catch (IOException) { } + catch (AggregateException aex) { - received.Close(); + PodeHelpers.HandleAggregateException(aex, Listener, PodeLoggingLevel.Error, true); + } + catch (Exception ex) + { + PodeHelpers.WriteException(ex, Listener); } - - // close the context - context.Dispose(true); - - // add args back to connections - ClearSocketAsyncEvent(args); - ReceiveConnections.Enqueue(args); - return; - } - - try - { - context.RenewTimeoutToken(); - Task.Factory.StartNew(() => context.Receive(), context.ContextTimeoutToken.Token); - } - catch (OperationCanceledException) { } - catch (IOException) { } - catch (AggregateException aex) - { - PodeHelpers.HandleAggregateException(aex, Listener, PodeLoggingLevel.Error, true); - } - catch (Exception ex) - { - PodeHelpers.WriteException(ex, Listener); } // add args back to connections ClearSocketAsyncEvent(args); - ReceiveConnections.Enqueue(args); + AcceptConnections.Enqueue(args); } - public void HandleContext(PodeContext context) + public async Task HandleContext(PodeContext context) { try { @@ -287,7 +234,7 @@ public void HandleContext(PodeContext context) { if (!context.IsWebSocketUpgraded) { - context.UpgradeWebSocket(); + await context.UpgradeWebSocket().ConfigureAwait(false); process = false; context.Dispose(); } @@ -350,36 +297,11 @@ private SocketAsyncEventArgs NewAcceptConnection() } } - private SocketAsyncEventArgs NewReceiveConnection() - { - lock (ReceiveConnections) - { - var args = new SocketAsyncEventArgs(); - args.Completed += new EventHandler(Receive_Completed); - return args; - } - } - - private SocketAsyncEventArgs GetReceiveConnection() - { - if (!ReceiveConnections.TryDequeue(out SocketAsyncEventArgs args)) - { - args = NewReceiveConnection(); - } - - return args; - } - private void Accept_Completed(object sender, SocketAsyncEventArgs e) { ProcessAccept(e); } - private void Receive_Completed(object sender, SocketAsyncEventArgs e) - { - ProcessReceive(e); - } - private void AddPendingSocket(Socket socket) { lock (PendingSockets) @@ -392,7 +314,7 @@ private void AddPendingSocket(Socket socket) } } - private void RemovePendingSocket(Socket socket) + public void RemovePendingSocket(Socket socket) { lock (PendingSockets) { @@ -468,8 +390,8 @@ public static void CloseSocket(Socket socket) private void ClearSocketAsyncEvent(SocketAsyncEventArgs e) { - e.AcceptSocket = default(Socket); - e.UserToken = default(object); + e.AcceptSocket = default; + e.UserToken = default; } public new bool Equals(object obj) diff --git a/src/Listener/PodeTcpRequest.cs b/src/Listener/PodeTcpRequest.cs index af2a60aea..c48982538 100644 --- a/src/Listener/PodeTcpRequest.cs +++ b/src/Listener/PodeTcpRequest.cs @@ -1,4 +1,6 @@ using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; namespace Pode { @@ -22,11 +24,11 @@ public string Body public override bool CloseImmediately { - get => (IsDisposed || RawBody == default(byte[]) || RawBody.Length == 0); + get => IsDisposed || RawBody == default(byte[]) || RawBody.Length == 0; } - public PodeTcpRequest(Socket socket, PodeSocket podeSocket) - : base(socket, podeSocket) + public PodeTcpRequest(Socket socket, PodeSocket podeSocket, PodeContext context) + : base(socket, podeSocket, context) { IsKeepAlive = true; Type = PodeProtocolType.Tcp; @@ -43,31 +45,30 @@ protected override bool ValidateInput(byte[] bytes) // expect to end with ? if (Context.PodeSocket.CRLFMessageEnd) { - return (bytes[bytes.Length - 2] == (byte)13 - && bytes[bytes.Length - 1] == (byte)10); + return bytes[bytes.Length - 2] == PodeHelpers.CARRIAGE_RETURN_BYTE + && bytes[bytes.Length - 1] == PodeHelpers.NEW_LINE_BYTE; } return true; } - protected override bool Parse(byte[] bytes) + protected override Task Parse(byte[] bytes, CancellationToken cancellationToken) { - RawBody = bytes; + // check if the request is cancelled + cancellationToken.ThrowIfCancellationRequested(); - // if there are no bytes, return (0 bytes read means we can close the socket) - if (bytes.Length == 0) - { - return true; - } + // set the raw body + RawBody = bytes; - return true; + // return that we're done + return Task.FromResult(true); } public void Reset() { PodeHelpers.WriteErrorMessage($"Request reset", Context.Listener, PodeLoggingLevel.Verbose, Context); _body = string.Empty; - RawBody = default(byte[]); + RawBody = default; } public void Close() @@ -77,7 +78,7 @@ public void Close() public override void Dispose() { - RawBody = default(byte[]); + RawBody = default; _body = string.Empty; base.Dispose(); } diff --git a/src/Listener/PodeWatcher.cs b/src/Listener/PodeWatcher.cs index eebdec709..ed3134cb6 100644 --- a/src/Listener/PodeWatcher.cs +++ b/src/Listener/PodeWatcher.cs @@ -11,7 +11,7 @@ public class PodeWatcher : PodeConnector public PodeItemQueue FileEvents { get; private set; } - public PodeWatcher(CancellationToken cancellationToken = default(CancellationToken)) + public PodeWatcher(CancellationToken cancellationToken = default) : base(cancellationToken) { FileWatchers = new List(); @@ -24,7 +24,7 @@ public void AddFileWatcher(PodeFileWatcher watcher) FileWatchers.Add(watcher); } - public Task GetFileEventAsync(CancellationToken cancellationToken = default(CancellationToken)) + public Task GetFileEventAsync(CancellationToken cancellationToken = default) { return FileEvents.GetAsync(cancellationToken); } diff --git a/src/Listener/PodeWebSocket.cs b/src/Listener/PodeWebSocket.cs index 10d6a75f9..87cf4ed79 100644 --- a/src/Listener/PodeWebSocket.cs +++ b/src/Listener/PodeWebSocket.cs @@ -17,27 +17,23 @@ public class PodeWebSocket : IDisposable public string ContentType { get; private set; } public bool IsConnected { - get => (WebSocket != default(ClientWebSocket) && WebSocket.State == WebSocketState.Open); + get => WebSocket != default(ClientWebSocket) && WebSocket.State == WebSocketState.Open; } private ClientWebSocket WebSocket; - public PodeWebSocket(string name, string url, string contentType) + public PodeWebSocket(string name, string url, string contentType, PodeReceiver receiver) { Name = name; URL = new Uri(url); + Receiver = receiver; ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/json" : contentType; } - public void BindReceiver(PodeReceiver receiver) - { - Receiver = receiver; - } - - public async void Connect() + public async Task Connect() { if (IsConnected) { @@ -46,29 +42,29 @@ public async void Connect() if (WebSocket != default(ClientWebSocket)) { - Disconnect(PodeWebSocketCloseFrom.Client); + await Disconnect(PodeWebSocketCloseFrom.Client).ConfigureAwait(false); WebSocket.Dispose(); } WebSocket = new ClientWebSocket(); WebSocket.Options.KeepAliveInterval = TimeSpan.FromSeconds(60); - await WebSocket.ConnectAsync(URL, Receiver.CancellationToken); - await Task.Factory.StartNew(Receive, Receiver.CancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default); + await WebSocket.ConnectAsync(URL, Receiver.CancellationToken).ConfigureAwait(false); + await Task.Factory.StartNew(Receive, Receiver.CancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).ConfigureAwait(false); } - public void Reconnect(string url) + public async Task Reconnect(string url) { if (!string.IsNullOrWhiteSpace(url)) { URL = new Uri(url); } - Disconnect(PodeWebSocketCloseFrom.Client); - Connect(); + await Disconnect(PodeWebSocketCloseFrom.Client).ConfigureAwait(false); + await Connect().ConfigureAwait(false); } - public async void Receive() + public async Task Receive() { var result = default(WebSocketReceiveResult); var buffer = _WebSocket.CreateClientBuffer(1024, 1024); @@ -80,7 +76,7 @@ public async void Receive() { do { - result = await WebSocket.ReceiveAsync(buffer, Receiver.CancellationToken); + result = await WebSocket.ReceiveAsync(buffer, Receiver.CancellationToken).ConfigureAwait(false); if (result.MessageType != WebSocketMessageType.Close) { bufferStream.Write(buffer.ToArray(), 0, result.Count); @@ -90,7 +86,7 @@ public async void Receive() if (result.MessageType == WebSocketMessageType.Close) { - Disconnect(PodeWebSocketCloseFrom.Server); + await Disconnect(PodeWebSocketCloseFrom.Server).ConfigureAwait(false); break; } @@ -105,7 +101,8 @@ public async void Receive() bufferStream = new MemoryStream(); } } - catch (TaskCanceledException) {} + catch (OperationCanceledException) { } + catch (IOException) { } catch (WebSocketException ex) { PodeHelpers.WriteException(ex, Receiver, PodeLoggingLevel.Debug); @@ -113,23 +110,27 @@ public async void Receive() } finally { - bufferStream.Dispose(); - bufferStream = default(MemoryStream); - buffer = default(ArraySegment); + if (bufferStream != default) + { + bufferStream.Dispose(); + bufferStream = default; + } + + buffer = default; } } - public void Send(string message, WebSocketMessageType type = WebSocketMessageType.Text) + public async Task Send(string message, WebSocketMessageType type = WebSocketMessageType.Text) { if (!IsConnected) { return; } - WebSocket.SendAsync(new ArraySegment(Encoding.UTF8.GetBytes(message)), type, true, Receiver.CancellationToken).Wait(); + await WebSocket.SendAsync(new ArraySegment(Encoding.UTF8.GetBytes(message)), type, true, Receiver.CancellationToken).ConfigureAwait(false); } - public void Disconnect(PodeWebSocketCloseFrom closeFrom) + public async Task Disconnect(PodeWebSocketCloseFrom closeFrom) { if (WebSocket == default(ClientWebSocket)) { @@ -143,26 +144,26 @@ public void Disconnect(PodeWebSocketCloseFrom closeFrom) // only close output in client closing if (closeFrom == PodeWebSocketCloseFrom.Client) { - WebSocket.CloseOutputAsync(WebSocketCloseStatus.Empty, string.Empty, CancellationToken.None).Wait(); + await WebSocket.CloseOutputAsync(WebSocketCloseStatus.Empty, string.Empty, CancellationToken.None).ConfigureAwait(false); } // if the server is closing, or client and netcore, then close properly if (closeFrom == PodeWebSocketCloseFrom.Server || !PodeHelpers.IsNetFramework) { - WebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None).Wait(); + await WebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None).ConfigureAwait(false); } PodeHelpers.WriteErrorMessage($"Closed client web socket: {Name}", Receiver, PodeLoggingLevel.Verbose); } WebSocket.Dispose(); - WebSocket = default(ClientWebSocket); + WebSocket = default; PodeHelpers.WriteErrorMessage($"Disconnected client web socket: {Name}", Receiver, PodeLoggingLevel.Verbose); } public void Dispose() { - Disconnect(PodeWebSocketCloseFrom.Client); + Disconnect(PodeWebSocketCloseFrom.Client).Wait(); } } } \ No newline at end of file diff --git a/src/Listener/PodeWebSocketRequest.cs b/src/Listener/PodeWebSocketRequest.cs index 7305d9666..39f623f29 100644 --- a/src/Listener/PodeWebSocketRequest.cs +++ b/src/Listener/PodeWebSocketRequest.cs @@ -13,7 +13,7 @@ public class PodeWebSocketRequest : IDisposable public int ContentLength { - get => (RawBody == default(byte[]) ? 0 : RawBody.Length); + get => RawBody == default(byte[]) ? 0 : RawBody.Length; } private string _body = string.Empty; @@ -39,7 +39,7 @@ public PodeWebSocketRequest(PodeWebSocket webSocket, MemoryStream bytes) public void Dispose() { WebSocket.Receiver.RemoveProcessingWebSocketRequest(this); - RawBody = default(byte[]); + RawBody = default; _body = string.Empty; } diff --git a/src/Locales/ar/Pode.psd1 b/src/Locales/ar/Pode.psd1 new file mode 100644 index 000000000..978646dab --- /dev/null +++ b/src/Locales/ar/Pode.psd1 @@ -0,0 +1,294 @@ +@{ + schemaValidationRequiresPowerShell610ExceptionMessage = 'يتطلب التحقق من صحة المخطط إصدار PowerShell 6.1.0 أو أحدث.' + customAccessPathOrScriptBlockRequiredExceptionMessage = 'مطلوب مسار أو ScriptBlock للحصول على قيم الوصول المخصصة.' + operationIdMustBeUniqueForArrayExceptionMessage = 'يجب أن يكون OperationID: {0} فريدًا ولا يمكن تطبيقه على مصفوفة.' + endpointNotDefinedForRedirectingExceptionMessage = "لم يتم تعريف نقطة نهاية باسم '{0}' لإعادة التوجيه." + filesHaveChangedMessage = 'تم تغيير الملفات التالية:' + iisAspnetcoreTokenMissingExceptionMessage = 'IIS ASPNETCORE_TOKEN مفقود.' + minValueGreaterThanMaxExceptionMessage = 'يجب ألا تكون القيمة الدنيا {0} أكبر من القيمة القصوى.' + noLogicPassedForRouteExceptionMessage = 'لم يتم تمرير منطق للمسار: {0}' + scriptPathDoesNotExistExceptionMessage = 'مسار البرنامج النصي غير موجود: {0}' + mutexAlreadyExistsExceptionMessage = 'يوجد بالفعل Mutex بالاسم التالي: {0}' + listeningOnEndpointsMessage = 'الاستماع على {0} نقطة(نقاط) النهاية التالية [{1} خيط(خيوط)]:' + unsupportedFunctionInServerlessContextExceptionMessage = 'الدالة {0} غير مدعومة في سياق بدون خادم.' + expectedNoJwtSignatureSuppliedExceptionMessage = 'لم يكن من المتوقع توفير توقيع JWT.' + secretAlreadyMountedExceptionMessage = "تم تثبيت سر بالاسم '{0}' بالفعل." + failedToAcquireLockExceptionMessage = 'فشل في الحصول على قفل على الكائن.' + noPathSuppliedForStaticRouteExceptionMessage = '[{0}]: لم يتم توفير مسار للمسار الثابت.' + invalidHostnameSuppliedExceptionMessage = 'اسم المضيف المقدم غير صالح: {0}' + authMethodAlreadyDefinedExceptionMessage = 'طريقة المصادقة محددة بالفعل: {0}' + csrfCookieRequiresSecretExceptionMessage = "عند استخدام ملفات تعريف الارتباط لـ CSRF، يكون السر مطلوبًا. يمكنك تقديم سر أو تعيين السر العالمي لملف تعريف الارتباط - (Set-PodeCookieSecret '' -Global)" + nonEmptyScriptBlockRequiredForPageRouteExceptionMessage = 'مطلوب ScriptBlock غير فارغ لإنشاء مسار الصفحة.' + noPropertiesMutuallyExclusiveExceptionMessage = "المعامل 'NoProperties' يتعارض مع 'Properties' و 'MinProperties' و 'MaxProperties'." + incompatiblePodeDllExceptionMessage = 'يتم تحميل إصدار غير متوافق من Pode.DLL {0}. الإصدار {1} مطلوب. افتح جلسة Powershell/pwsh جديدة وأعد المحاولة.' + accessMethodDoesNotExistExceptionMessage = 'طريقة الوصول غير موجودة: {0}.' + scheduleAlreadyDefinedExceptionMessage = '[الجدول الزمني] {0}: الجدول الزمني معرف بالفعل.' + secondsValueCannotBeZeroOrLessExceptionMessage = 'لا يمكن أن تكون قيمة الثواني 0 أو أقل لـ {0}' + pathToLoadNotFoundExceptionMessage = 'لم يتم العثور على المسار لتحميل {0}: {1}' + failedToImportModuleExceptionMessage = 'فشل في استيراد الوحدة: {0}' + endpointNotExistExceptionMessage = "نقطة النهاية مع البروتوكول '{0}' والعنوان '{1}' أو العنوان المحلي '{2}' غير موجودة." + terminatingMessage = 'إنهاء...' + noCommandsSuppliedToConvertToRoutesExceptionMessage = 'لم يتم توفير أي أوامر لتحويلها إلى طرق.' + invalidTaskTypeExceptionMessage = 'نوع المهمة غير صالح، المتوقع إما [System.Threading.Tasks.Task] أو [hashtable].' + alreadyConnectedToWebSocketExceptionMessage = "متصل بالفعل بـ WebSocket بالاسم '{0}'" + crlfMessageEndCheckOnlySupportedOnTcpEndpointsExceptionMessage = 'فحص نهاية الرسالة CRLF مدعوم فقط على نقاط النهاية TCP.' + testPodeOAComponentSchemaNeedToBeEnabledExceptionMessage = "يجب تمكين 'Test-PodeOAComponentSchema' باستخدام 'Enable-PodeOpenApi -EnableSchemaValidation'" + adModuleNotInstalledExceptionMessage = 'وحدة Active Directory غير مثبتة.' + cronExpressionInvalidExceptionMessage = 'يجب أن تتكون تعبير Cron من 5 أجزاء فقط: {0}' + noSessionToSetOnResponseExceptionMessage = 'لا توجد جلسة متاحة لتعيينها على الاستجابة.' + valueOutOfRangeExceptionMessage = "القيمة '{0}' لـ {1} غير صالحة، يجب أن تكون بين {2} و {3}" + loggingMethodAlreadyDefinedExceptionMessage = 'تم تعريف طريقة التسجيل بالفعل: {0}' + noSecretForHmac256ExceptionMessage = 'لم يتم تقديم أي سر لتجزئة HMAC256.' + eolPowerShellWarningMessage = '[تحذير] لم يتم اختبار Pode {0} على PowerShell {1}، حيث أنه نهاية العمر.' + runspacePoolFailedToLoadExceptionMessage = 'فشل تحميل RunspacePool لـ {0}.' + noEventRegisteredExceptionMessage = 'لا يوجد حدث {0} مسجل: {1}' + scheduleCannotHaveNegativeLimitExceptionMessage = '[الجدول الزمني] {0}: لا يمكن أن يكون له حد سلبي.' + openApiRequestStyleInvalidForParameterExceptionMessage = 'لا يمكن أن يكون نمط الطلب OpenApi {0} لمعلمة {1}.' + openApiDocumentNotCompliantExceptionMessage = 'مستند OpenAPI غير متوافق.' + taskDoesNotExistExceptionMessage = "المهمة '{0}' غير موجودة." + scopedVariableNotFoundExceptionMessage = 'لم يتم العثور على المتغير المحدد: {0}' + sessionsRequiredForCsrfExceptionMessage = 'الجلسات مطلوبة لاستخدام CSRF إلا إذا كنت ترغب في استخدام ملفات تعريف الارتباط.' + nonEmptyScriptBlockRequiredForLoggingMethodExceptionMessage = 'مطلوب ScriptBlock غير فارغ لطريقة التسجيل.' + credentialsPassedWildcardForHeadersLiteralExceptionMessage = 'عند تمرير بيانات الاعتماد، سيتم اعتبار العلامة * للعنوان كـ سلسلة نصية حرفية وليس كعلامة.' + podeNotInitializedExceptionMessage = 'لم يتم تهيئة Pode.' + multipleEndpointsForGuiMessage = 'تم تعريف نقاط نهاية متعددة، سيتم استخدام الأولى فقط للواجهة الرسومية.' + operationIdMustBeUniqueExceptionMessage = 'يجب أن يكون OperationID: {0} فريدًا.' + invalidJsonJwtExceptionMessage = 'تم العثور على قيمة JSON غير صالحة في JWT' + noAlgorithmInJwtHeaderExceptionMessage = 'لم يتم توفير أي خوارزمية في رأس JWT.' + openApiVersionPropertyMandatoryExceptionMessage = 'خاصية إصدار OpenApi إلزامية.' + limitValueCannotBeZeroOrLessExceptionMessage = 'لا يمكن أن تكون القيمة الحدية 0 أو أقل لـ {0}' + timerDoesNotExistExceptionMessage = "المؤقت '{0}' غير موجود." + openApiGenerationDocumentErrorMessage = 'خطأ في مستند إنشاء OpenAPI:' + routeAlreadyContainsCustomAccessExceptionMessage = "المسار '[{0}] {1}' يحتوي بالفعل على وصول مخصص باسم '{2}'" + maximumConcurrentWebSocketThreadsLessThanMinimumExceptionMessage = 'لا يمكن أن يكون الحد الأقصى لمؤشرات ترابط WebSocket المتزامنة أقل من الحد الأدنى {0}، ولكن تم الحصول عليه: {1}' + middlewareAlreadyDefinedExceptionMessage = '[Middleware] {0}: تم تعريف الوسيط بالفعل.' + invalidAtomCharacterExceptionMessage = 'حرف الذرة غير صالح: {0}' + invalidCronAtomFormatExceptionMessage = 'تم العثور على تنسيق cron غير صالح: {0}' + cacheStorageNotFoundForRetrieveExceptionMessage = "لم يتم العثور على مخزن ذاكرة التخزين المؤقت بالاسم '{0}' عند محاولة استرجاع العنصر المخزن مؤقتًا '{1}'" + headerMustHaveNameInEncodingContextExceptionMessage = 'يجب أن يحتوي الرأس على اسم عند استخدامه في سياق الترميز.' + moduleDoesNotContainFunctionExceptionMessage = 'الوحدة {0} لا تحتوي على الوظيفة {1} لتحويلها إلى مسار.' + pathToIconForGuiDoesNotExistExceptionMessage = 'المسار إلى الأيقونة للواجهة الرسومية غير موجود: {0}' + noTitleSuppliedForPageExceptionMessage = 'لم يتم توفير عنوان للصفحة {0}.' + certificateSuppliedForNonHttpsWssEndpointExceptionMessage = 'تم توفير شهادة لنقطة نهاية غير HTTPS/WSS.' + cannotLockNullObjectExceptionMessage = 'لا يمكن قفل كائن فارغ.' + showPodeGuiOnlyAvailableOnWindowsExceptionMessage = 'Show-PodeGui متاح حاليًا فقط لـ Windows PowerShell و PowerShell 7+ على Windows.' + unlockSecretButNoScriptBlockExceptionMessage = 'تم تقديم سر الفتح لنوع خزنة سرية مخصصة، ولكن لم يتم تقديم ScriptBlock الفتح.' + invalidIpAddressExceptionMessage = 'عنوان IP المقدم غير صالح: {0}' + maxDaysInvalidExceptionMessage = 'يجب أن يكون MaxDays 0 أو أكبر، ولكن تم الحصول على: {0}' + noRemoveScriptBlockForVaultExceptionMessage = "لم يتم تقديم ScriptBlock الإزالة لإزالة الأسرار من الخزنة '{0}'" + noSecretExpectedForNoSignatureExceptionMessage = 'لم يكن من المتوقع تقديم أي سر لعدم وجود توقيع.' + noCertificateFoundExceptionMessage = "لم يتم العثور على شهادة في {0}{1} لـ '{2}'" + minValueInvalidExceptionMessage = "القيمة الدنيا '{0}' لـ {1} غير صالحة، يجب أن تكون أكبر من/أو تساوي {2}" + accessRequiresAuthenticationOnRoutesExceptionMessage = 'يتطلب الوصول توفير المصادقة على الطرق.' + noSecretForHmac384ExceptionMessage = 'لم يتم تقديم أي سر لتجزئة HMAC384.' + windowsLocalAuthSupportIsForWindowsOnlyExceptionMessage = 'دعم المصادقة المحلية لـ Windows هو فقط لنظام Windows.' + definitionTagNotDefinedExceptionMessage = 'لم يتم تعريف علامة التعريف {0}.' + noComponentInDefinitionExceptionMessage = 'لا توجد مكون من نوع {0} باسم {1} متاح في تعريف {2}.' + noSmtpHandlersDefinedExceptionMessage = 'لم يتم تعريف أي معالجات SMTP.' + sessionMiddlewareAlreadyInitializedExceptionMessage = 'تم تهيئة Session Middleware بالفعل.' + reusableComponentPathItemsNotAvailableInOpenApi30ExceptionMessage = "ميزة المكون القابل لإعادة الاستخدام 'pathItems' غير متوفرة في OpenAPI v3.0." + wildcardHeadersIncompatibleWithAutoHeadersExceptionMessage = 'العلامة * للعنوان غير متوافقة مع مفتاح AutoHeaders.' + noDataForFileUploadedExceptionMessage = "لا توجد بيانات للملف '{0}' الذي تم تحميله في الطلب." + sseOnlyConfiguredOnEventStreamAcceptHeaderExceptionMessage = 'يمكن تكوين SSE فقط على الطلبات التي تحتوي على قيمة رأس Accept النص/تيار الأحداث.' + noSessionAvailableToSaveExceptionMessage = 'لا توجد جلسة متاحة للحفظ.' + pathParameterRequiresRequiredSwitchExceptionMessage = "إذا كانت موقع المعلمة هو 'Path'، فإن المعلمة التبديل 'Required' إلزامية." + noOpenApiUrlSuppliedExceptionMessage = 'لم يتم توفير عنوان URL OpenAPI لـ {0}.' + maximumConcurrentSchedulesInvalidExceptionMessage = 'يجب أن تكون الجداول الزمنية المتزامنة القصوى >=1 ولكن تم الحصول على: {0}' + snapinsSupportedOnWindowsPowershellOnlyExceptionMessage = 'Snapins مدعومة فقط في Windows PowerShell.' + eventViewerLoggingSupportedOnWindowsOnlyExceptionMessage = 'تسجيل عارض الأحداث مدعوم فقط على Windows.' + parametersMutuallyExclusiveExceptionMessage = "المعاملات '{0}' و '{1}' متعارضة." + pathItemsFeatureNotSupportedInOpenApi30ExceptionMessage = 'ميزة PathItems غير مدعومة في OpenAPI v3.0.x' + openApiParameterRequiresNameExceptionMessage = 'يتطلب معلمة OpenApi اسمًا محددًا.' + maximumConcurrentTasksLessThanMinimumExceptionMessage = 'لا يمكن أن يكون الحد الأقصى للمهام المتزامنة أقل من الحد الأدنى {0}، ولكن تم الحصول عليه: {1}' + noSemaphoreFoundExceptionMessage = "لم يتم العثور على Semaphore باسم '{0}'" + singleValueForIntervalExceptionMessage = 'يمكنك تقديم قيمة {0} واحدة فقط عند استخدام الفواصل الزمنية.' + jwtNotYetValidExceptionMessage = 'JWT غير صالح للاستخدام بعد.' + verbAlreadyDefinedForUrlExceptionMessage = '[الفعل] {0}: تم التعريف بالفعل لـ {1}' + noSecretNamedMountedExceptionMessage = "لم يتم تثبيت أي سر بالاسم '{0}'." + moduleOrVersionNotFoundExceptionMessage = 'لم يتم العثور على الوحدة أو الإصدار على {0}: {1}@{2}' + noScriptBlockSuppliedExceptionMessage = 'لم يتم تقديم أي ScriptBlock.' + noSecretVaultRegisteredExceptionMessage = "لم يتم تسجيل خزينة سرية بالاسم '{0}'." + nameRequiredForEndpointIfRedirectToSuppliedExceptionMessage = 'مطلوب اسم لنقطة النهاية إذا تم توفير معامل RedirectTo.' + openApiLicenseObjectRequiresNameExceptionMessage = "يتطلب كائن OpenAPI 'license' الخاصية 'name'. استخدم المعامل -LicenseName." + sourcePathDoesNotExistForStaticRouteExceptionMessage = '{0}: مسار المصدر المقدم للمسار الثابت غير موجود: {1}' + noNameForWebSocketDisconnectExceptionMessage = 'لا يوجد اسم لفصل WebSocket من المزود.' + certificateExpiredExceptionMessage = "الشهادة '{0}' منتهية الصلاحية: {1}" + secretVaultUnlockExpiryDateInPastExceptionMessage = 'تاريخ انتهاء صلاحية فتح مخزن الأسرار في الماضي (UTC): {0}' + invalidWebExceptionTypeExceptionMessage = 'الاستثناء من نوع غير صالح، يجب أن يكون إما WebException أو HttpRequestException، ولكن تم الحصول عليه: {0}' + invalidSecretValueTypeExceptionMessage = 'قيمة السر من نوع غير صالح. الأنواع المتوقعة: String، SecureString، HashTable، Byte[]، أو PSCredential. ولكن تم الحصول عليه: {0}' + explicitTlsModeOnlySupportedOnSmtpsTcpsEndpointsExceptionMessage = 'وضع TLS الصريح مدعوم فقط على نقاط النهاية SMTPS و TCPS.' + discriminatorMappingRequiresDiscriminatorPropertyExceptionMessage = "يمكن استخدام المعامل 'DiscriminatorMapping' فقط عندما تكون خاصية 'DiscriminatorProperty' موجودة." + scriptErrorExceptionMessage = "خطأ '{0}' في البرنامج النصي {1} {2} (السطر {3}) الحرف {4} أثناء تنفيذ {5} على الكائن {6} 'الصنف: {7} الصنف الأساسي: {8}" + cannotSupplyIntervalForQuarterExceptionMessage = 'لا يمكن توفير قيمة الفاصل الزمني لكل ربع.' + scheduleEndTimeMustBeInFutureExceptionMessage = '[الجدول الزمني] {0}: يجب أن تكون قيمة EndTime في المستقبل.' + invalidJwtSignatureSuppliedExceptionMessage = 'توقيع JWT المقدم غير صالح.' + noSetScriptBlockForVaultExceptionMessage = "لم يتم تقديم ScriptBlock الإعداد لتحديث/إنشاء الأسرار في الخزنة '{0}'" + accessMethodNotExistForMergingExceptionMessage = 'طريقة الوصول غير موجودة للدمج: {0}' + defaultAuthNotInListExceptionMessage = "المصادقة الافتراضية '{0}' غير موجودة في قائمة المصادقة المقدمة." + parameterHasNoNameExceptionMessage = "لا يحتوي المعامل على اسم. يرجى إعطاء هذا المكون اسمًا باستخدام معامل 'Name'." + methodPathAlreadyDefinedForUrlExceptionMessage = '[{0}] {1}: تم التعريف بالفعل لـ {2}' + fileWatcherAlreadyDefinedExceptionMessage = "تم تعريف مراقب الملفات باسم '{0}' بالفعل." + noServiceHandlersDefinedExceptionMessage = 'لم يتم تعريف أي معالجات خدمة.' + secretRequiredForCustomSessionStorageExceptionMessage = 'مطلوب سر عند استخدام تخزين الجلسة المخصص.' + secretManagementModuleNotInstalledExceptionMessage = 'وحدة Microsoft.PowerShell.SecretManagement غير مثبتة.' + noPathSuppliedForRouteExceptionMessage = 'لم يتم توفير مسار للطريق.' + validationOfAnyOfSchemaNotSupportedExceptionMessage = "التحقق من مخطط يتضمن 'أي منها' غير مدعوم." + iisAuthSupportIsForWindowsOnlyExceptionMessage = 'دعم مصادقة IIS هو فقط لنظام Windows.' + oauth2InnerSchemeInvalidExceptionMessage = 'يمكن أن تكون OAuth2 InnerScheme إما مصادقة Basic أو Form فقط، ولكن تم الحصول على: {0}' + noRoutePathSuppliedForPageExceptionMessage = 'لم يتم توفير مسار للصفحة {0}.' + cacheStorageNotFoundForExistsExceptionMessage = "لم يتم العثور على مخزن ذاكرة التخزين المؤقت بالاسم '{0}' عند محاولة التحقق مما إذا كان العنصر المخزن مؤقتًا '{1}' موجودًا." + handlerAlreadyDefinedExceptionMessage = '[{0}] {1}: تم تعريف المعالج بالفعل.' + sessionsNotConfiguredExceptionMessage = 'لم يتم تكوين الجلسات.' + propertiesTypeObjectAssociationExceptionMessage = 'يمكن ربط خصائص النوع Object فقط بـ {0}.' + sessionsRequiredForSessionPersistentAuthExceptionMessage = 'تتطلب المصادقة المستمرة للجلسة جلسات.' + invalidPathWildcardOrDirectoryExceptionMessage = 'لا يمكن أن يكون المسار المقدم عبارة عن حرف بدل أو دليل: {0}' + accessMethodAlreadyDefinedExceptionMessage = 'طريقة الوصول معرفة بالفعل: {0}' + parametersValueOrExternalValueMandatoryExceptionMessage = "المعاملات 'Value' أو 'ExternalValue' إلزامية." + maximumConcurrentTasksInvalidExceptionMessage = 'يجب أن يكون الحد الأقصى للمهام المتزامنة >=1، ولكن تم الحصول عليه: {0}' + cannotCreatePropertyWithoutTypeExceptionMessage = 'لا يمكن إنشاء الخاصية لأنه لم يتم تعريف نوع.' + authMethodNotExistForMergingExceptionMessage = 'طريقة المصادقة غير موجودة للدمج: {0}' + maxValueInvalidExceptionMessage = "القيمة القصوى '{0}' لـ {1} غير صالحة، يجب أن تكون أقل من/أو تساوي {2}" + endpointAlreadyDefinedExceptionMessage = "تم تعريف نقطة نهاية باسم '{0}' بالفعل." + eventAlreadyRegisteredExceptionMessage = 'الحدث {0} مسجل بالفعل: {1}' + parameterNotSuppliedInRequestExceptionMessage = "لم يتم توفير معلمة باسم '{0}' في الطلب أو لا توجد بيانات متاحة." + cacheStorageNotFoundForSetExceptionMessage = "لم يتم العثور على مخزن ذاكرة التخزين المؤقت بالاسم '{0}' عند محاولة تعيين العنصر المخزن مؤقتًا '{1}'" + methodPathAlreadyDefinedExceptionMessage = '[{0}] {1}: تم التعريف بالفعل.' + errorLoggingAlreadyEnabledExceptionMessage = 'تم تمكين تسجيل الأخطاء بالفعل.' + valueForUsingVariableNotFoundExceptionMessage = "لم يتم العثور على قيمة لـ '`$using:{0}'." + rapidPdfDoesNotSupportOpenApi31ExceptionMessage = 'أداة الوثائق RapidPdf لا تدعم OpenAPI 3.1' + oauth2ClientSecretRequiredExceptionMessage = 'تتطلب OAuth2 سر العميل عند عدم استخدام PKCE.' + invalidBase64JwtExceptionMessage = 'تم العثور على قيمة مشفرة بتنسيق Base64 غير صالحة في JWT' + noSessionToCalculateDataHashExceptionMessage = 'لا توجد جلسة متاحة لحساب تجزئة البيانات.' + cacheStorageNotFoundForRemoveExceptionMessage = "لم يتم العثور على مخزن ذاكرة التخزين المؤقت بالاسم '{0}' عند محاولة إزالة العنصر المخزن مؤقتًا '{1}'" + csrfMiddlewareNotInitializedExceptionMessage = 'لم يتم تهيئة CSRF Middleware.' + infoTitleMandatoryMessage = 'info.title إلزامي.' + typeCanOnlyBeAssociatedWithObjectExceptionMessage = 'النوع {0} يمكن ربطه فقط بجسم.' + userFileDoesNotExistExceptionMessage = 'ملف المستخدم غير موجود: {0}' + routeParameterNeedsValidScriptblockExceptionMessage = 'المعامل Route يتطلب ScriptBlock صالح وغير فارغ.' + nextTriggerCalculationErrorExceptionMessage = 'يبدو أن هناك خطأ ما أثناء محاولة حساب تاريخ المشغل التالي: {0}' + cannotLockValueTypeExceptionMessage = 'لا يمكن قفل [ValueType].' + failedToCreateOpenSslCertExceptionMessage = 'فشل في إنشاء شهادة OpenSSL: {0}' + jwtExpiredExceptionMessage = 'انتهت صلاحية JWT.' + openingGuiMessage = 'جارٍ فتح الواجهة الرسومية.' + multiTypePropertiesRequireOpenApi31ExceptionMessage = 'تتطلب خصائص الأنواع المتعددة إصدار OpenApi 3.1 أو أعلى.' + noNameForWebSocketRemoveExceptionMessage = 'لا يوجد اسم لإزالة WebSocket من المزود.' + maxSizeInvalidExceptionMessage = 'يجب أن يكون MaxSize 0 أو أكبر، ولكن تم الحصول على: {0}' + iisShutdownMessage = '(إيقاف تشغيل IIS)' + cannotUnlockValueTypeExceptionMessage = 'لا يمكن فتح [ValueType].' + noJwtSignatureForAlgorithmExceptionMessage = 'لم يتم توفير توقيع JWT لـ {0}.' + maximumConcurrentWebSocketThreadsInvalidExceptionMessage = 'يجب أن يكون الحد الأقصى لمؤشرات ترابط WebSocket المتزامنة >=1، ولكن تم الحصول عليه: {0}' + acknowledgeMessageOnlySupportedOnSmtpTcpEndpointsExceptionMessage = 'رسالة الإقرار مدعومة فقط على نقاط النهاية SMTP و TCP.' + failedToConnectToUrlExceptionMessage = 'فشل الاتصال بعنوان URL: {0}' + failedToAcquireMutexOwnershipExceptionMessage = 'فشل في الحصول على ملكية Mutex. اسم Mutex: {0}' + sessionsRequiredForOAuth2WithPKCEExceptionMessage = 'تتطلب OAuth2 مع PKCE جلسات.' + failedToConnectToWebSocketExceptionMessage = 'فشل الاتصال بـ WebSocket: {0}' + unsupportedObjectExceptionMessage = 'الكائن غير مدعوم' + failedToParseAddressExceptionMessage = "فشل في تحليل '{0}' كعنوان IP/مضيف:منفذ صالح" + mustBeRunningWithAdminPrivilegesExceptionMessage = 'يجب التشغيل بامتيازات المسؤول للاستماع إلى العناوين غير المحلية.' + specificationMessage = 'مواصفات' + cacheStorageNotFoundForClearExceptionMessage = "لم يتم العثور على مخزن ذاكرة التخزين المؤقت بالاسم '{0}' عند محاولة مسح الذاكرة المؤقتة." + restartingServerMessage = 'إعادة تشغيل الخادم...' + cannotSupplyIntervalWhenEveryIsNoneExceptionMessage = "لا يمكن توفير فترة زمنية عندما يكون المعامل 'Every' مضبوطًا على None." + unsupportedJwtAlgorithmExceptionMessage = 'خوارزمية JWT غير مدعومة حاليًا: {0}' + websocketsNotConfiguredForSignalMessagesExceptionMessage = 'لم يتم تهيئة WebSockets لإرسال رسائل الإشارة.' + invalidLogicTypeInHashtableMiddlewareExceptionMessage = 'مكون Middleware من نوع Hashtable المقدم يحتوي على نوع منطق غير صالح. كان المتوقع ScriptBlock، ولكن تم الحصول عليه: {0}' + maximumConcurrentSchedulesLessThanMinimumExceptionMessage = 'لا يمكن أن تكون الجداول الزمنية المتزامنة القصوى أقل من الحد الأدنى {0} ولكن تم الحصول على: {1}' + failedToAcquireSemaphoreOwnershipExceptionMessage = 'فشل في الحصول على ملكية Semaphore. اسم Semaphore: {0}' + propertiesParameterWithoutNameExceptionMessage = 'لا يمكن استخدام معلمات الخصائص إذا لم يكن لدى الخاصية اسم.' + customSessionStorageMethodNotImplementedExceptionMessage = "تخزين الجلسة المخصص لا ينفذ الطريقة المطلوبة '{0}()'." + authenticationMethodDoesNotExistExceptionMessage = 'طريقة المصادقة غير موجودة: {0}' + webhooksFeatureNotSupportedInOpenApi30ExceptionMessage = 'ميزة Webhooks غير مدعومة في OpenAPI v3.0.x' + invalidContentTypeForSchemaExceptionMessage = "'content-type' غير صالح في المخطط: {0}" + noUnlockScriptBlockForVaultExceptionMessage = "لم يتم تقديم ScriptBlock الفتح لفتح الخزنة '{0}'" + definitionTagMessage = 'تعريف {0}:' + failedToOpenRunspacePoolExceptionMessage = 'فشل في فتح RunspacePool: {0}' + failedToCloseRunspacePoolExceptionMessage = 'فشل في إغلاق RunspacePool: {0}' + verbNoLogicPassedExceptionMessage = '[الفعل] {0}: لم يتم تمرير أي منطق' + noMutexFoundExceptionMessage = "لم يتم العثور على Mutex باسم '{0}'" + documentationMessage = 'توثيق' + timerAlreadyDefinedExceptionMessage = '[المؤقت] {0}: المؤقت معرف بالفعل.' + invalidPortExceptionMessage = 'لا يمكن أن يكون المنفذ سالبًا: {0}' + viewsFolderNameAlreadyExistsExceptionMessage = 'اسم مجلد العرض موجود بالفعل: {0}' + noNameForWebSocketResetExceptionMessage = 'لا يوجد اسم لإعادة تعيين WebSocket من المزود.' + mergeDefaultAuthNotInListExceptionMessage = "المصادقة MergeDefault '{0}' غير موجودة في قائمة المصادقة المقدمة." + descriptionRequiredExceptionMessage = 'مطلوب وصف للمسار: {0} الاستجابة: {1}' + pageNameShouldBeAlphaNumericExceptionMessage = 'يجب أن يكون اسم الصفحة قيمة أبجدية رقمية صالحة: {0}' + defaultValueNotBooleanOrEnumExceptionMessage = 'القيمة الافتراضية ليست من نوع boolean وليست جزءًا من التعداد.' + openApiComponentSchemaDoesNotExistExceptionMessage = 'مخطط مكون OpenApi {0} غير موجود.' + timerParameterMustBeGreaterThanZeroExceptionMessage = '[المؤقت] {0}: {1} يجب أن يكون أكبر من 0.' + taskTimedOutExceptionMessage = 'انتهت المهلة الزمنية للمهمة بعد {0}ms.' + scheduleStartTimeAfterEndTimeExceptionMessage = "[الجدول الزمني] {0}: لا يمكن أن يكون 'StartTime' بعد 'EndTime'" + infoVersionMandatoryMessage = 'info.version إلزامي.' + cannotUnlockNullObjectExceptionMessage = 'لا يمكن فتح كائن فارغ.' + nonEmptyScriptBlockRequiredForCustomAuthExceptionMessage = 'مطلوب ScriptBlock غير فارغ لخطة المصادقة المخصصة.' + nonEmptyScriptBlockRequiredForAuthMethodExceptionMessage = 'مطلوب ScriptBlock غير فارغ لطريقة المصادقة.' + validationOfOneOfSchemaNotSupportedExceptionMessage = "التحقق من مخطط يتضمن 'واحد منها' غير مدعوم." + routeParameterCannotBeNullExceptionMessage = "لا يمكن أن يكون المعامل 'Route' فارغًا." + cacheStorageAlreadyExistsExceptionMessage = "مخزن ذاكرة التخزين المؤقت بالاسم '{0}' موجود بالفعل." + loggingMethodRequiresValidScriptBlockExceptionMessage = "تتطلب طريقة الإخراج المقدمة لطريقة التسجيل '{0}' ScriptBlock صالح." + scopedVariableAlreadyDefinedExceptionMessage = 'المتغير المحدد بالفعل معرف: {0}' + oauth2RequiresAuthorizeUrlExceptionMessage = 'تتطلب OAuth2 توفير عنوان URL للتفويض.' + pathNotExistExceptionMessage = 'المسار غير موجود: {0}' + noDomainServerNameForWindowsAdAuthExceptionMessage = 'لم يتم توفير اسم خادم المجال لمصادقة Windows AD.' + suppliedDateAfterScheduleEndTimeExceptionMessage = 'التاريخ المقدم بعد وقت انتهاء الجدول الزمني في {0}' + wildcardMethodsIncompatibleWithAutoMethodsExceptionMessage = 'العلامة * للطرق غير متوافقة مع مفتاح AutoMethods.' + cannotSupplyIntervalForYearExceptionMessage = 'لا يمكن توفير قيمة الفاصل الزمني لكل سنة.' + missingComponentsMessage = 'المكون (المكونات) المفقود' + invalidStrictTransportSecurityDurationExceptionMessage = 'تم توفير مدة Strict-Transport-Security غير صالحة: {0}. يجب أن تكون أكبر من 0.' + noSecretForHmac512ExceptionMessage = 'لم يتم تقديم أي سر لتجزئة HMAC512.' + daysInMonthExceededExceptionMessage = 'يحتوي {0} على {1} أيام فقط، ولكن تم توفير {2}.' + nonEmptyScriptBlockRequiredForCustomLoggingExceptionMessage = 'مطلوب ScriptBlock غير فارغ لطريقة إخراج السجل المخصصة.' + encodingAttributeOnlyAppliesToMultipartExceptionMessage = 'ينطبق سمة الترميز فقط على نصوص الطلبات multipart و application/x-www-form-urlencoded.' + suppliedDateBeforeScheduleStartTimeExceptionMessage = 'التاريخ المقدم قبل وقت بدء الجدول الزمني في {0}' + unlockSecretRequiredExceptionMessage = "خاصية 'UnlockSecret' مطلوبة عند استخدام Microsoft.PowerShell.SecretStore" + noLogicPassedForMethodRouteExceptionMessage = '[{0}] {1}: لم يتم تمرير منطق.' + bodyParserAlreadyDefinedForContentTypeExceptionMessage = 'تم تعريف محلل الجسم لنوع المحتوى {0} بالفعل.' + invalidJwtSuppliedExceptionMessage = 'JWT المقدم غير صالح.' + sessionsRequiredForFlashMessagesExceptionMessage = 'الجلسات مطلوبة لاستخدام رسائل الفلاش.' + semaphoreAlreadyExistsExceptionMessage = 'يوجد بالفعل Semaphore بالاسم التالي: {0}' + invalidJwtHeaderAlgorithmSuppliedExceptionMessage = 'خوارزمية رأس JWT المقدمة غير صالحة.' + oauth2ProviderDoesNotSupportPasswordGrantTypeExceptionMessage = "مزود OAuth2 لا يدعم نوع المنحة 'password' المطلوبة لاستخدام InnerScheme." + invalidAliasFoundExceptionMessage = 'تم العثور على اسم مستعار غير صالح {0}: {1}' + scheduleDoesNotExistExceptionMessage = "الجدول الزمني '{0}' غير موجود." + accessMethodNotExistExceptionMessage = 'طريقة الوصول غير موجودة: {0}' + oauth2ProviderDoesNotSupportCodeResponseTypeExceptionMessage = "مزود OAuth2 لا يدعم نوع الاستجابة 'code'." + untestedPowerShellVersionWarningMessage = '[تحذير] لم يتم اختبار Pode {0} على PowerShell {1}، حيث لم يكن متاحًا عند إصدار Pode.' + secretVaultAlreadyRegisteredAutoImportExceptionMessage = "تم تسجيل خزنة سرية باسم '{0}' بالفعل أثناء استيراد الخزن السرية تلقائيًا." + schemeRequiresValidScriptBlockExceptionMessage = "تتطلب الخطة المقدمة لمحقق المصادقة '{0}' ScriptBlock صالح." + serverLoopingMessage = 'تكرار الخادم كل {0} ثانية' + certificateThumbprintsNameSupportedOnWindowsExceptionMessage = 'بصمات الإبهام/الاسم للشهادة مدعومة فقط على Windows.' + sseConnectionNameRequiredExceptionMessage = "مطلوب اسم اتصال SSE، إما من -Name أو `$WebEvent.Sse.Name" + invalidMiddlewareTypeExceptionMessage = 'أحد مكونات Middleware المقدمة من نوع غير صالح. كان المتوقع إما ScriptBlock أو Hashtable، ولكن تم الحصول عليه: {0}' + noSecretForJwtSignatureExceptionMessage = 'لم يتم تقديم أي سر لتوقيع JWT.' + modulePathDoesNotExistExceptionMessage = 'مسار الوحدة غير موجود: {0}' + taskAlreadyDefinedExceptionMessage = '[المهمة] {0}: المهمة معرفة بالفعل.' + verbAlreadyDefinedExceptionMessage = '[الفعل] {0}: تم التعريف بالفعل' + clientCertificatesOnlySupportedOnHttpsEndpointsExceptionMessage = 'الشهادات العميلة مدعومة فقط على نقاط النهاية HTTPS.' + endpointNameNotExistExceptionMessage = "نقطة النهاية بالاسم '{0}' غير موجودة." + middlewareNoLogicSuppliedExceptionMessage = '[Middleware]: لم يتم توفير أي منطق في ScriptBlock.' + scriptBlockRequiredForMergingUsersExceptionMessage = 'مطلوب ScriptBlock لدمج عدة مستخدمين مصادق عليهم في كائن واحد عندما تكون Valid هي All.' + secretVaultAlreadyRegisteredExceptionMessage = "تم تسجيل مخزن الأسرار بالاسم '{0}' بالفعل{1}." + deprecatedTitleVersionDescriptionWarningMessage = "تحذير: العنوان، الإصدار والوصف في 'Enable-PodeOpenApi' مهمل. يرجى استخدام 'Add-PodeOAInfo' بدلاً من ذلك." + undefinedOpenApiReferencesMessage = 'مراجع OpenAPI غير معرّفة:' + doneMessage = 'تم' + swaggerEditorDoesNotSupportOpenApi31ExceptionMessage = 'هذا الإصدار من Swagger-Editor لا يدعم OpenAPI 3.1' + durationMustBeZeroOrGreaterExceptionMessage = 'يجب أن تكون المدة 0 أو أكبر، ولكن تم الحصول عليها: {0}s' + viewsPathDoesNotExistExceptionMessage = 'مسار العرض غير موجود: {0}' + discriminatorIncompatibleWithAllOfExceptionMessage = "المعامل 'Discriminator' غير متوافق مع 'allOf'." + noNameForWebSocketSendMessageExceptionMessage = 'لا يوجد اسم لإرسال رسالة إلى WebSocket المزود.' + hashtableMiddlewareNoLogicExceptionMessage = 'مكون Middleware من نوع Hashtable المقدم لا يحتوي على منطق معرف.' + openApiInfoMessage = 'معلومات OpenAPI:' + invalidSchemeForAuthValidatorExceptionMessage = "تتطلب الخطة '{0}' المقدمة لمحقق المصادقة '{1}' ScriptBlock صالح." + sseFailedToBroadcastExceptionMessage = 'فشل بث SSE بسبب مستوى البث SSE المحدد لـ {0}: {1}' + adModuleWindowsOnlyExceptionMessage = 'وحدة Active Directory متاحة فقط على نظام Windows.' + requestLoggingAlreadyEnabledExceptionMessage = 'تم تمكين تسجيل الطلبات بالفعل.' + invalidAccessControlMaxAgeDurationExceptionMessage = 'مدة Access-Control-Max-Age غير صالحة المقدمة: {0}. يجب أن تكون أكبر من 0.' + openApiDefinitionAlreadyExistsExceptionMessage = 'تعريف OpenAPI باسم {0} موجود بالفعل.' + renamePodeOADefinitionTagExceptionMessage = "لا يمكن استخدام Rename-PodeOADefinitionTag داخل Select-PodeOADefinition 'ScriptBlock'." + taskProcessDoesNotExistExceptionMessage = 'عملية المهمة غير موجودة: {0}' + scheduleProcessDoesNotExistExceptionMessage = 'عملية الجدول الزمني غير موجودة: {0}' + definitionTagChangeNotAllowedExceptionMessage = 'لا يمكن تغيير علامة التعريف لمسار.' + getRequestBodyNotAllowedExceptionMessage = 'لا يمكن أن تحتوي عمليات {0} على محتوى الطلب.' + fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "الدالة '{0}' لا تقبل مصفوفة كمدخل لأنبوب البيانات." + unsupportedStreamCompressionEncodingExceptionMessage = 'تشفير الضغط غير مدعوم للتشفير {0}' +} diff --git a/src/Locales/de/Pode.psd1 b/src/Locales/de/Pode.psd1 new file mode 100644 index 000000000..4d70c92aa --- /dev/null +++ b/src/Locales/de/Pode.psd1 @@ -0,0 +1,294 @@ +@{ + schemaValidationRequiresPowerShell610ExceptionMessage = 'Die Schema-Validierung erfordert PowerShell Version 6.1.0 oder höher.' + customAccessPathOrScriptBlockRequiredExceptionMessage = 'Ein Pfad oder ScriptBlock ist erforderlich, um die benutzerdefinierten Zugriffswerte zu beziehen.' + operationIdMustBeUniqueForArrayExceptionMessage = 'OperationID: {0} muss eindeutig sein und kann nicht auf ein Array angewendet werden.' + endpointNotDefinedForRedirectingExceptionMessage = "Ein Endpunkt mit dem Namen '{0}' wurde nicht für die Weiterleitung definiert." + filesHaveChangedMessage = 'Die folgenden Dateien wurden geändert:' + iisAspnetcoreTokenMissingExceptionMessage = 'Das IIS-ASPNETCORE_TOKEN fehlt.' + minValueGreaterThanMaxExceptionMessage = 'Der Mindestwert für {0} darf nicht größer als der Maximalwert sein.' + noLogicPassedForRouteExceptionMessage = 'Keine Logik für Route übergeben: {0}' + scriptPathDoesNotExistExceptionMessage = 'Der Skriptpfad existiert nicht: {0}' + mutexAlreadyExistsExceptionMessage = 'Ein Mutex mit folgendem Namen existiert bereits: {0}' + listeningOnEndpointsMessage = 'Lauschen auf den folgenden {0} Endpunkt(en) [{1} Thread(s)]:' + unsupportedFunctionInServerlessContextExceptionMessage = 'Die Funktion {0} wird in einem serverlosen Kontext nicht unterstützt.' + expectedNoJwtSignatureSuppliedExceptionMessage = 'Es wurde keine JWT-Signatur erwartet.' + secretAlreadyMountedExceptionMessage = "Ein Geheimnis mit dem Namen '{0}' wurde bereits eingebunden." + failedToAcquireLockExceptionMessage = 'Sperre des Objekts konnte nicht erworben werden.' + noPathSuppliedForStaticRouteExceptionMessage = '[{0}]: Kein Pfad für statische Route angegeben.' + invalidHostnameSuppliedExceptionMessage = 'Der angegebene Hostname ist ungültig: {0}' + authMethodAlreadyDefinedExceptionMessage = 'Authentifizierungsmethode bereits definiert: {0}' + csrfCookieRequiresSecretExceptionMessage = "Beim Verwenden von Cookies für CSRF ist ein Geheimnis erforderlich. Sie können ein Geheimnis angeben oder das globale Cookie-Geheimnis festlegen - (Set-PodeCookieSecret '' -Global)" + nonEmptyScriptBlockRequiredForPageRouteExceptionMessage = 'Ein nicht leerer ScriptBlock ist erforderlich, um eine Seitenroute zu erstellen.' + noPropertiesMutuallyExclusiveExceptionMessage = "Der Parameter 'NoProperties' schließt 'Properties', 'MinProperties' und 'MaxProperties' gegenseitig aus." + incompatiblePodeDllExceptionMessage = 'Eine vorhandene inkompatible Pode.DLL-Version {0} ist geladen. Version {1} wird benötigt. Öffnen Sie eine neue PowerShell/pwsh-Sitzung und versuchen Sie es erneut.' + accessMethodDoesNotExistExceptionMessage = 'Zugriffsmethode existiert nicht: {0}.' + scheduleAlreadyDefinedExceptionMessage = '[Aufgabenplaner] {0}: Aufgabenplaner bereits definiert.' + secondsValueCannotBeZeroOrLessExceptionMessage = 'Der Sekundenwert darf für {0} nicht 0 oder weniger sein.' + pathToLoadNotFoundExceptionMessage = 'Pfad zum Laden von {0} nicht gefunden: {1}' + failedToImportModuleExceptionMessage = 'Modulimport fehlgeschlagen: {0}' + endpointNotExistExceptionMessage = "Der Endpunkt mit dem Protokoll '{0}' und der Adresse '{1}' oder der lokalen Adresse '{2}' existiert nicht" + terminatingMessage = 'Beenden...' + noCommandsSuppliedToConvertToRoutesExceptionMessage = 'Keine Befehle zur Umwandlung in Routen bereitgestellt.' + invalidTaskTypeExceptionMessage = 'Aufgabentyp ist ungültig, erwartet entweder [System.Threading.Tasks.Task] oder [hashtable]' + alreadyConnectedToWebSocketExceptionMessage = "Bereits mit dem WebSocket mit dem Namen '{0}' verbunden" + crlfMessageEndCheckOnlySupportedOnTcpEndpointsExceptionMessage = 'Die CRLF-Nachrichtenendprüfung wird nur auf TCP-Endpunkten unterstützt.' + testPodeOAComponentSchemaNeedToBeEnabledExceptionMessage = "'Test-PodeOAComponentSchema' muss mit 'Enable-PodeOpenApi -EnableSchemaValidation' aktiviert werden." + adModuleNotInstalledExceptionMessage = 'Das Active Directory-Modul ist nicht installiert.' + cronExpressionInvalidExceptionMessage = 'Die Cron-Ausdruck sollte nur aus 5 Teilen bestehen: {0}' + noSessionToSetOnResponseExceptionMessage = 'Keine Sitzung verfügbar, die auf die Antwort gesetzt werden kann.' + valueOutOfRangeExceptionMessage = "Wert '{0}' für {1} ist ungültig, sollte zwischen {2} und {3} liegen" + loggingMethodAlreadyDefinedExceptionMessage = 'Logging-Methode bereits definiert: {0}' + noSecretForHmac256ExceptionMessage = 'Es wurde kein Geheimnis für den HMAC256-Hash angegeben.' + eolPowerShellWarningMessage = '[WARNUNG] Pode {0} wurde nicht auf PowerShell {1} getestet, da es das Ende des Lebenszyklus erreicht hat.' + runspacePoolFailedToLoadExceptionMessage = '{0} RunspacePool konnte nicht geladen werden.' + noEventRegisteredExceptionMessage = 'Kein Ereignis {0} registriert: {1}' + scheduleCannotHaveNegativeLimitExceptionMessage = '[Aufgabenplaner] {0}: Kann kein negatives Limit haben.' + openApiRequestStyleInvalidForParameterExceptionMessage = 'Der OpenApi-Anfragestil kann für einen {1}-Parameter nicht {0} sein.' + openApiDocumentNotCompliantExceptionMessage = 'Das OpenAPI-Dokument ist nicht konform.' + taskDoesNotExistExceptionMessage = "Aufgabe '{0}' existiert nicht." + scopedVariableNotFoundExceptionMessage = 'Bereichsvariable nicht gefunden: {0}' + sessionsRequiredForCsrfExceptionMessage = 'Sitzungen sind erforderlich, um CSRF zu verwenden, es sei denn, Sie möchten Cookies verwenden.' + nonEmptyScriptBlockRequiredForLoggingMethodExceptionMessage = 'Ein nicht leerer ScriptBlock ist für die Protokollierungsmethode erforderlich.' + credentialsPassedWildcardForHeadersLiteralExceptionMessage = 'Wenn Anmeldeinformationen übergeben werden, wird das *-Wildcard für Header als Literalzeichenfolge und nicht als Platzhalter verwendet.' + podeNotInitializedExceptionMessage = 'Pode wurde nicht initialisiert.' + multipleEndpointsForGuiMessage = 'Mehrere Endpunkte definiert, es wird nur der erste für die GUI verwendet.' + operationIdMustBeUniqueExceptionMessage = 'OperationID: {0} muss eindeutig sein.' + invalidJsonJwtExceptionMessage = 'Ungültiger JSON-Wert in JWT gefunden' + noAlgorithmInJwtHeaderExceptionMessage = 'Kein Algorithmus im JWT-Header angegeben.' + openApiVersionPropertyMandatoryExceptionMessage = 'Die Eigenschaft OpenApi-Version ist obligatorisch.' + limitValueCannotBeZeroOrLessExceptionMessage = 'Der Grenzwert darf für {0} nicht 0 oder weniger sein.' + timerDoesNotExistExceptionMessage = "Timer '{0}' existiert nicht." + openApiGenerationDocumentErrorMessage = 'Fehler beim Generieren des OpenAPI-Dokuments:' + routeAlreadyContainsCustomAccessExceptionMessage = "Die Route '[{0}] {1}' enthält bereits einen benutzerdefinierten Zugriff mit dem Namen '{2}'." + maximumConcurrentWebSocketThreadsLessThanMinimumExceptionMessage = 'Die maximale Anzahl gleichzeitiger WebSocket-Threads darf nicht kleiner als das Minimum von {0} sein, aber erhalten: {1}' + middlewareAlreadyDefinedExceptionMessage = '[Middleware] {0}: Middleware bereits definiert.' + invalidAtomCharacterExceptionMessage = 'Ungültiges Atomzeichen: {0}' + invalidCronAtomFormatExceptionMessage = 'Ungültiges Cron-Atom-Format gefunden: {0}' + cacheStorageNotFoundForRetrieveExceptionMessage = "Der Cache-Speicher mit dem Namen '{0}' wurde nicht gefunden, als versucht wurde, das zwischengespeicherte Element '{1}' abzurufen." + headerMustHaveNameInEncodingContextExceptionMessage = 'Ein Header muss einen Namen haben, wenn er im Codierungskontext verwendet wird.' + moduleDoesNotContainFunctionExceptionMessage = 'Modul {0} enthält keine Funktion {1} zur Umwandlung in eine Route.' + pathToIconForGuiDoesNotExistExceptionMessage = 'Der Pfad zum Symbol für die GUI existiert nicht: {0}' + noTitleSuppliedForPageExceptionMessage = 'Kein Titel für die Seite {0} angegeben.' + certificateSuppliedForNonHttpsWssEndpointExceptionMessage = 'Zertifikat für Nicht-HTTPS/WSS-Endpunkt bereitgestellt.' + cannotLockNullObjectExceptionMessage = 'Kann ein null-Objekt nicht sperren.' + showPodeGuiOnlyAvailableOnWindowsExceptionMessage = 'Show-PodeGui ist derzeit nur für Windows PowerShell und PowerShell 7+ unter Windows verfügbar.' + unlockSecretButNoScriptBlockExceptionMessage = 'Unlock secret für benutzerdefinierten Secret Vault-Typ angegeben, aber kein Unlock ScriptBlock bereitgestellt.' + invalidIpAddressExceptionMessage = 'Die angegebene IP-Adresse ist ungültig: {0}' + maxDaysInvalidExceptionMessage = 'MaxDays muss 0 oder größer sein, aber erhalten: {0}' + noRemoveScriptBlockForVaultExceptionMessage = "Kein Remove ScriptBlock für das Entfernen von Geheimnissen im Tresor '{0}' bereitgestellt." + noSecretExpectedForNoSignatureExceptionMessage = 'Es wurde erwartet, dass kein Geheimnis für keine Signatur angegeben wird.' + noCertificateFoundExceptionMessage = "Es wurde kein Zertifikat in {0}{1} für '{2}' gefunden." + minValueInvalidExceptionMessage = "Der Mindestwert '{0}' für {1} ist ungültig, sollte größer oder gleich {2} sein" + accessRequiresAuthenticationOnRoutesExceptionMessage = 'Der Zugriff erfordert eine Authentifizierung auf den Routen.' + noSecretForHmac384ExceptionMessage = 'Es wurde kein Geheimnis für den HMAC384-Hash angegeben.' + windowsLocalAuthSupportIsForWindowsOnlyExceptionMessage = 'Die Unterstützung der lokalen Windows-Authentifizierung gilt nur für Windows.' + definitionTagNotDefinedExceptionMessage = 'Definitionstag {0} ist nicht definiert.' + noComponentInDefinitionExceptionMessage = 'Es ist keine Komponente des Typs {0} mit dem Namen {1} in der Definition {2} verfügbar.' + noSmtpHandlersDefinedExceptionMessage = 'Es wurden keine SMTP-Handler definiert.' + sessionMiddlewareAlreadyInitializedExceptionMessage = 'Session Middleware wurde bereits initialisiert.' + reusableComponentPathItemsNotAvailableInOpenApi30ExceptionMessage = "Die wiederverwendbare Komponente 'pathItems' ist in OpenAPI v3.0 nicht verfügbar." + wildcardHeadersIncompatibleWithAutoHeadersExceptionMessage = 'Das *-Wildcard für Header ist nicht mit dem AutoHeaders-Schalter kompatibel.' + noDataForFileUploadedExceptionMessage = "Keine Daten für die Datei '{0}' wurden in der Anfrage hochgeladen." + sseOnlyConfiguredOnEventStreamAcceptHeaderExceptionMessage = 'SSE kann nur auf Anfragen mit einem Accept-Header-Wert von text/event-stream konfiguriert werden.' + noSessionAvailableToSaveExceptionMessage = 'Keine Sitzung verfügbar zum Speichern.' + pathParameterRequiresRequiredSwitchExceptionMessage = "Wenn der Parameterstandort 'Path' ist, ist der Schalterparameter 'Required' erforderlich." + noOpenApiUrlSuppliedExceptionMessage = 'Keine OpenAPI-URL für {0} angegeben.' + maximumConcurrentSchedulesInvalidExceptionMessage = 'Maximale gleichzeitige Zeitpläne müssen >=1 sein, aber erhalten: {0}' + snapinsSupportedOnWindowsPowershellOnlyExceptionMessage = 'Snapins werden nur in Windows PowerShell unterstützt.' + eventViewerLoggingSupportedOnWindowsOnlyExceptionMessage = 'Das Protokollieren im Ereignisanzeige wird nur auf Windows unterstützt.' + parametersMutuallyExclusiveExceptionMessage = "Die Parameter '{0}' und '{1}' schließen sich gegenseitig aus." + pathItemsFeatureNotSupportedInOpenApi30ExceptionMessage = 'Das PathItems-Feature wird in OpenAPI v3.0.x nicht unterstützt.' + openApiParameterRequiresNameExceptionMessage = 'Der OpenApi-Parameter erfordert einen angegebenen Namen.' + maximumConcurrentTasksLessThanMinimumExceptionMessage = 'Die maximale Anzahl gleichzeitiger Aufgaben darf nicht kleiner als das Minimum von {0} sein, aber erhalten: {1}' + noSemaphoreFoundExceptionMessage = "Kein Semaphor mit dem Namen '{0}' gefunden." + singleValueForIntervalExceptionMessage = 'Sie können nur einen einzelnen {0}-Wert angeben, wenn Sie Intervalle verwenden.' + jwtNotYetValidExceptionMessage = 'Der JWT ist noch nicht gültig.' + verbAlreadyDefinedForUrlExceptionMessage = '[Verb] {0}: Bereits für {1} definiert.' + noSecretNamedMountedExceptionMessage = "Kein Geheimnis mit dem Namen '{0}' wurde eingebunden." + moduleOrVersionNotFoundExceptionMessage = 'Modul oder Version nicht gefunden auf {0}: {1}@{2}' + noScriptBlockSuppliedExceptionMessage = 'Kein Skriptblock angegeben.' + noSecretVaultRegisteredExceptionMessage = "Kein Geheimnistresor mit dem Namen '{0}' registriert." + nameRequiredForEndpointIfRedirectToSuppliedExceptionMessage = 'Ein Name ist für den Endpunkt erforderlich, wenn der RedirectTo-Parameter angegeben ist.' + openApiLicenseObjectRequiresNameExceptionMessage = "Das OpenAPI-Objekt 'license' erfordert die Eigenschaft 'name'. Verwenden Sie den Parameter -LicenseName." + sourcePathDoesNotExistForStaticRouteExceptionMessage = '{0}: Der angegebene Quellpfad für die statische Route existiert nicht: {1}' + noNameForWebSocketDisconnectExceptionMessage = 'Kein Name für die Trennung vom WebSocket angegeben.' + certificateExpiredExceptionMessage = "Das Zertifikat '{0}' ist abgelaufen: {1}" + secretVaultUnlockExpiryDateInPastExceptionMessage = 'Das Ablaufdatum zum Entsperren des Geheimnis-Tresors liegt in der Vergangenheit (UTC): {0}' + invalidWebExceptionTypeExceptionMessage = 'Die Ausnahme hat einen ungültigen Typ. Er sollte entweder WebException oder HttpRequestException sein, aber es wurde {0} erhalten' + invalidSecretValueTypeExceptionMessage = 'Der Geheimniswert hat einen ungültigen Typ. Erwartete Typen: String, SecureString, HashTable, Byte[] oder PSCredential. Aber erhalten wurde: {0}.' + explicitTlsModeOnlySupportedOnSmtpsTcpsEndpointsExceptionMessage = 'Der explizite TLS-Modus wird nur auf SMTPS- und TCPS-Endpunkten unterstützt.' + discriminatorMappingRequiresDiscriminatorPropertyExceptionMessage = "Der Parameter 'DiscriminatorMapping' kann nur verwendet werden, wenn 'DiscriminatorProperty' vorhanden ist." + scriptErrorExceptionMessage = "Fehler '{0}' im Skript {1} {2} (Zeile {3}) Zeichen {4} beim Ausführen von {5} auf {6} Objekt '{7}' Klasse: {8} Basisklasse: {9}" + cannotSupplyIntervalForQuarterExceptionMessage = 'Ein Intervallwert kann nicht für jedes Quartal angegeben werden.' + scheduleEndTimeMustBeInFutureExceptionMessage = '[Aufgabenplaner] {0}: Der Wert für EndTime muss in der Zukunft liegen.' + invalidJwtSignatureSuppliedExceptionMessage = 'Ungültige JWT-Signatur angegeben.' + noSetScriptBlockForVaultExceptionMessage = "Kein Set ScriptBlock für das Aktualisieren/Erstellen von Geheimnissen im Tresor '{0}' bereitgestellt." + accessMethodNotExistForMergingExceptionMessage = 'Zugriffsmethode zum Zusammenführen nicht vorhanden: {0}.' + defaultAuthNotInListExceptionMessage = "Die Standardauthentifizierung '{0}' befindet sich nicht in der angegebenen Authentifizierungsliste." + parameterHasNoNameExceptionMessage = "Der Parameter hat keinen Namen. Bitte geben Sie dieser Komponente einen Namen mit dem 'Name'-Parameter." + methodPathAlreadyDefinedForUrlExceptionMessage = '[{0}] {1}: Bereits für {2} definiert.' + fileWatcherAlreadyDefinedExceptionMessage = "Ein Dateiwächter mit dem Namen '{0}' wurde bereits definiert." + noServiceHandlersDefinedExceptionMessage = 'Es wurden keine Service-Handler definiert.' + secretRequiredForCustomSessionStorageExceptionMessage = 'Ein Geheimnis ist erforderlich, wenn benutzerdefinierter Sitzungspeicher verwendet wird.' + secretManagementModuleNotInstalledExceptionMessage = 'Das Modul Microsoft.PowerShell.SecretManagement ist nicht installiert.' + noPathSuppliedForRouteExceptionMessage = 'Kein Pfad für die Route bereitgestellt.' + validationOfAnyOfSchemaNotSupportedExceptionMessage = "Die Validierung eines Schemas, das 'anyof' enthält, wird nicht unterstützt." + iisAuthSupportIsForWindowsOnlyExceptionMessage = 'Die IIS-Authentifizierungsunterstützung gilt nur für Windows.' + oauth2InnerSchemeInvalidExceptionMessage = 'OAuth2 InnerScheme kann nur entweder Basic oder Form-Authentifizierung sein, aber erhalten: {0}' + noRoutePathSuppliedForPageExceptionMessage = 'Kein Routenpfad für die Seite {0} angegeben.' + cacheStorageNotFoundForExistsExceptionMessage = "Der Cache-Speicher mit dem Namen '{0}' wurde nicht gefunden, als versucht wurde zu überprüfen, ob das zwischengespeicherte Element '{1}' existiert." + handlerAlreadyDefinedExceptionMessage = '[{0}] {1}: Handler bereits definiert.' + sessionsNotConfiguredExceptionMessage = 'Sitzungen wurden nicht konfiguriert.' + propertiesTypeObjectAssociationExceptionMessage = 'Nur Eigenschaften vom Typ Object können mit {0} verknüpft werden.' + sessionsRequiredForSessionPersistentAuthExceptionMessage = 'Sitzungen sind erforderlich, um die sitzungsbeständige Authentifizierung zu verwenden.' + invalidPathWildcardOrDirectoryExceptionMessage = 'Der angegebene Pfad darf kein Platzhalter oder Verzeichnis sein: {0}' + accessMethodAlreadyDefinedExceptionMessage = 'Zugriffsmethode bereits definiert: {0}.' + parametersValueOrExternalValueMandatoryExceptionMessage = "Die Parameter 'Value' oder 'ExternalValue' sind obligatorisch." + maximumConcurrentTasksInvalidExceptionMessage = 'Die maximale Anzahl gleichzeitiger Aufgaben muss >=1 sein, aber erhalten: {0}' + cannotCreatePropertyWithoutTypeExceptionMessage = 'Die Eigenschaft kann nicht erstellt werden, weil kein Typ definiert ist.' + authMethodNotExistForMergingExceptionMessage = 'Die Authentifizierungsmethode existiert nicht zum Zusammenführen: {0}' + maxValueInvalidExceptionMessage = "Der Maximalwert '{0}' für {1} ist ungültig, sollte kleiner oder gleich {2} sein" + endpointAlreadyDefinedExceptionMessage = "Ein Endpunkt mit dem Namen '{0}' wurde bereits definiert." + eventAlreadyRegisteredExceptionMessage = 'Ereignis {0} bereits registriert: {1}' + parameterNotSuppliedInRequestExceptionMessage = "Ein Parameter namens '{0}' wurde in der Anfrage nicht angegeben oder es sind keine Daten verfügbar." + cacheStorageNotFoundForSetExceptionMessage = "Der Cache-Speicher mit dem Namen '{0}' wurde nicht gefunden, als versucht wurde, das zwischengespeicherte Element '{1}' zu setzen." + methodPathAlreadyDefinedExceptionMessage = '[{0}] {1}: Bereits definiert.' + errorLoggingAlreadyEnabledExceptionMessage = 'Die Fehlerprotokollierung wurde bereits aktiviert.' + valueForUsingVariableNotFoundExceptionMessage = "Der Wert für '`$using:{0}' konnte nicht gefunden werden." + rapidPdfDoesNotSupportOpenApi31ExceptionMessage = 'Das Dokumentationstool RapidPdf unterstützt OpenAPI 3.1 nicht.' + oauth2ClientSecretRequiredExceptionMessage = 'OAuth2 erfordert ein Client Secret, wenn PKCE nicht verwendet wird.' + invalidBase64JwtExceptionMessage = 'Ungültiger Base64-codierter Wert in JWT gefunden' + noSessionToCalculateDataHashExceptionMessage = 'Keine Sitzung verfügbar, um den Datenhash zu berechnen.' + cacheStorageNotFoundForRemoveExceptionMessage = "Der Cache-Speicher mit dem Namen '{0}' wurde nicht gefunden, als versucht wurde, das zwischengespeicherte Element '{1}' zu entfernen." + csrfMiddlewareNotInitializedExceptionMessage = 'CSRF Middleware wurde nicht initialisiert.' + infoTitleMandatoryMessage = 'info.title ist obligatorisch.' + typeCanOnlyBeAssociatedWithObjectExceptionMessage = 'Der Typ {0} kann nur einem Objekt zugeordnet werden.' + userFileDoesNotExistExceptionMessage = 'Die Benutzerdaten-Datei existiert nicht: {0}' + routeParameterNeedsValidScriptblockExceptionMessage = 'Der Route-Parameter benötigt einen gültigen, nicht leeren ScriptBlock.' + nextTriggerCalculationErrorExceptionMessage = 'Es scheint, als ob beim Berechnen des nächsten Trigger-Datums und der nächsten Triggerzeit etwas schief gelaufen wäre: {0}' + cannotLockValueTypeExceptionMessage = 'Kann [ValueType] nicht sperren.' + failedToCreateOpenSslCertExceptionMessage = 'Erstellung des OpenSSL-Zertifikats fehlgeschlagen: {0}.' + jwtExpiredExceptionMessage = 'Der JWT ist abgelaufen.' + openingGuiMessage = 'Die GUI wird geöffnet.' + multiTypePropertiesRequireOpenApi31ExceptionMessage = 'Mehrfachtyp-Eigenschaften erfordern OpenApi-Version 3.1 oder höher.' + noNameForWebSocketRemoveExceptionMessage = 'Kein Name für das Entfernen des WebSocket angegeben.' + maxSizeInvalidExceptionMessage = 'MaxSize muss 0 oder größer sein, aber erhalten: {0}' + iisShutdownMessage = '(IIS Herunterfahren)' + cannotUnlockValueTypeExceptionMessage = 'Kann [ValueType] nicht entsperren.' + noJwtSignatureForAlgorithmExceptionMessage = 'Keine JWT-Signatur für {0} angegeben.' + maximumConcurrentWebSocketThreadsInvalidExceptionMessage = 'Die maximale Anzahl gleichzeitiger WebSocket-Threads muss >=1 sein, aber erhalten: {0}' + acknowledgeMessageOnlySupportedOnSmtpTcpEndpointsExceptionMessage = 'Die Bestätigungsnachricht wird nur auf SMTP- und TCP-Endpunkten unterstützt.' + failedToConnectToUrlExceptionMessage = 'Verbindung mit der URL fehlgeschlagen: {0}' + failedToAcquireMutexOwnershipExceptionMessage = 'Fehler beim Erwerb des Mutex-Besitzes. Mutex-Name: {0}' + sessionsRequiredForOAuth2WithPKCEExceptionMessage = 'Sitzungen sind erforderlich, um OAuth2 mit PKCE zu verwenden.' + failedToConnectToWebSocketExceptionMessage = 'Verbindung zum WebSocket fehlgeschlagen: {0}' + unsupportedObjectExceptionMessage = 'Nicht unterstütztes Objekt' + failedToParseAddressExceptionMessage = "Konnte '{0}' nicht als gültige IP/Host:Port-Adresse analysieren" + mustBeRunningWithAdminPrivilegesExceptionMessage = 'Muss mit Administratorrechten ausgeführt werden, um auf Nicht-Localhost-Adressen zu lauschen.' + specificationMessage = 'Spezifikation' + cacheStorageNotFoundForClearExceptionMessage = "Der Cache-Speicher mit dem Namen '{0}' wurde nicht gefunden, als versucht wurde, den Cache zu leeren." + restartingServerMessage = 'Server wird neu gestartet...' + cannotSupplyIntervalWhenEveryIsNoneExceptionMessage = "Ein Intervall kann nicht angegeben werden, wenn der Parameter 'Every' auf None gesetzt ist." + unsupportedJwtAlgorithmExceptionMessage = 'Der JWT-Algorithmus wird derzeit nicht unterstützt: {0}' + websocketsNotConfiguredForSignalMessagesExceptionMessage = 'WebSockets wurden nicht konfiguriert, um Signalnachrichten zu senden.' + invalidLogicTypeInHashtableMiddlewareExceptionMessage = 'Eine angegebene Hashtable-Middleware enthält einen ungültigen Logik-Typ. Erwartet wurde ein ScriptBlock, aber erhalten wurde: {0}.' + maximumConcurrentSchedulesLessThanMinimumExceptionMessage = 'Maximale gleichzeitige Zeitpläne dürfen nicht kleiner als das Minimum von {0} sein, aber erhalten: {1}' + failedToAcquireSemaphoreOwnershipExceptionMessage = 'Fehler beim Erwerb des Semaphor-Besitzes. Semaphor-Name: {0}' + propertiesParameterWithoutNameExceptionMessage = 'Die Eigenschaftsparameter können nicht verwendet werden, wenn die Eigenschaft keinen Namen hat.' + customSessionStorageMethodNotImplementedExceptionMessage = "Der benutzerdefinierte Sitzungspeicher implementiert die erforderliche Methode '{0}()' nicht." + authenticationMethodDoesNotExistExceptionMessage = 'Authentifizierungsmethode existiert nicht: {0}' + webhooksFeatureNotSupportedInOpenApi30ExceptionMessage = 'Das Webhooks-Feature wird in OpenAPI v3.0.x nicht unterstützt.' + invalidContentTypeForSchemaExceptionMessage = "Ungültiger 'content-type' im Schema gefunden: {0}" + noUnlockScriptBlockForVaultExceptionMessage = "Kein Unlock ScriptBlock für das Entsperren des Tresors '{0}' bereitgestellt." + definitionTagMessage = 'Definition {0}:' + failedToOpenRunspacePoolExceptionMessage = 'Fehler beim Öffnen des Runspace-Pools: {0}' + failedToCloseRunspacePoolExceptionMessage = 'Fehler beim Schließen des RunspacePools: {0}' + verbNoLogicPassedExceptionMessage = '[Verb] {0}: Keine Logik übergeben' + noMutexFoundExceptionMessage = "Kein Mutex mit dem Namen '{0}' gefunden." + documentationMessage = 'Dokumentation' + timerAlreadyDefinedExceptionMessage = '[Timer] {0}: Timer bereits definiert.' + invalidPortExceptionMessage = 'Der Port kann nicht negativ sein: {0}' + viewsFolderNameAlreadyExistsExceptionMessage = 'Der Name des Ansichtsordners existiert bereits: {0}' + noNameForWebSocketResetExceptionMessage = 'Kein Name für das Zurücksetzen des WebSocket angegeben.' + mergeDefaultAuthNotInListExceptionMessage = "Die MergeDefault-Authentifizierung '{0}' befindet sich nicht in der angegebenen Authentifizierungsliste." + descriptionRequiredExceptionMessage = 'Eine Beschreibung ist erforderlich für Pfad:{0} Antwort:{1}' + pageNameShouldBeAlphaNumericExceptionMessage = 'Der Seitenname sollte einen gültigen alphanumerischen Wert haben: {0}' + defaultValueNotBooleanOrEnumExceptionMessage = 'Der Standardwert ist kein Boolean und gehört nicht zum Enum.' + openApiComponentSchemaDoesNotExistExceptionMessage = 'Das OpenApi-Komponentenschema {0} existiert nicht.' + timerParameterMustBeGreaterThanZeroExceptionMessage = '[Timer] {0}: {1} muss größer als 0 sein.' + taskTimedOutExceptionMessage = 'Aufgabe ist nach {0}ms abgelaufen.' + scheduleStartTimeAfterEndTimeExceptionMessage = '[Aufgabenplaner] {0}: StartTime kann nicht nach EndTime liegen.' + infoVersionMandatoryMessage = 'info.version ist obligatorisch.' + cannotUnlockNullObjectExceptionMessage = 'Kann ein null-Objekt nicht entsperren.' + nonEmptyScriptBlockRequiredForCustomAuthExceptionMessage = 'Ein nicht leerer ScriptBlock ist für das benutzerdefinierte Authentifizierungsschema erforderlich.' + nonEmptyScriptBlockRequiredForAuthMethodExceptionMessage = 'Für die Authentifizierungsmethode ist ein nicht leerer ScriptBlock erforderlich.' + validationOfOneOfSchemaNotSupportedExceptionMessage = "Die Validierung eines Schemas, das 'oneof' enthält, wird nicht unterstützt." + routeParameterCannotBeNullExceptionMessage = "Der Parameter 'Route' darf nicht null sein." + cacheStorageAlreadyExistsExceptionMessage = "Ein Cache-Speicher mit dem Namen '{0}' existiert bereits." + loggingMethodRequiresValidScriptBlockExceptionMessage = "Die angegebene Ausgabemethode für die Logging-Methode '{0}' erfordert einen gültigen ScriptBlock." + scopedVariableAlreadyDefinedExceptionMessage = 'Die Bereichsvariable ist bereits definiert: {0}.' + oauth2RequiresAuthorizeUrlExceptionMessage = 'OAuth2 erfordert die Angabe einer Autorisierungs-URL.' + pathNotExistExceptionMessage = 'Pfad existiert nicht: {0}' + noDomainServerNameForWindowsAdAuthExceptionMessage = 'Es wurde kein Domänenservername für die Windows-AD-Authentifizierung angegeben.' + suppliedDateAfterScheduleEndTimeExceptionMessage = 'Das angegebene Datum liegt nach der Endzeit des Aufgabenplaners bei {0}' + wildcardMethodsIncompatibleWithAutoMethodsExceptionMessage = 'Das *-Wildcard für Methoden ist nicht mit dem AutoMethods-Schalter kompatibel.' + cannotSupplyIntervalForYearExceptionMessage = 'Ein Intervallwert kann nicht für jedes Jahr angegeben werden.' + missingComponentsMessage = 'Fehlende Komponente(n)' + invalidStrictTransportSecurityDurationExceptionMessage = 'Ungültige Strict-Transport-Security-Dauer angegeben: {0}. Sie sollte größer als 0 sein.' + noSecretForHmac512ExceptionMessage = 'Es wurde kein Geheimnis für den HMAC512-Hash angegeben.' + daysInMonthExceededExceptionMessage = '{0} hat nur {1} Tage, aber {2} wurden angegeben' + nonEmptyScriptBlockRequiredForCustomLoggingExceptionMessage = 'Ein nicht leerer ScriptBlock ist für die benutzerdefinierte Protokollierungsmethode erforderlich.' + encodingAttributeOnlyAppliesToMultipartExceptionMessage = 'Das Encoding-Attribut gilt nur für multipart und application/x-www-form-urlencoded Anfragekörper.' + suppliedDateBeforeScheduleStartTimeExceptionMessage = 'Das angegebene Datum liegt vor der Startzeit des Aufgabenplaners bei {0}' + unlockSecretRequiredExceptionMessage = "Eine 'UnlockSecret'-Eigenschaft ist erforderlich, wenn Microsoft.PowerShell.SecretStore verwendet wird." + noLogicPassedForMethodRouteExceptionMessage = '[{0}] {1}: Keine Logik übergeben.' + bodyParserAlreadyDefinedForContentTypeExceptionMessage = 'Für den Inhaltstyp {0} ist bereits ein Body-Parser definiert.' + invalidJwtSuppliedExceptionMessage = 'Ungültiger JWT angegeben.' + sessionsRequiredForFlashMessagesExceptionMessage = 'Sitzungen sind erforderlich, um Flash-Nachrichten zu verwenden.' + semaphoreAlreadyExistsExceptionMessage = 'Ein Semaphor mit folgendem Namen existiert bereits: {0}' + invalidJwtHeaderAlgorithmSuppliedExceptionMessage = 'Ungültiger JWT-Header-Algorithmus angegeben.' + oauth2ProviderDoesNotSupportPasswordGrantTypeExceptionMessage = "Der OAuth2-Anbieter unterstützt den für die Verwendung eines InnerScheme erforderlichen 'password'-Grant-Typ nicht." + invalidAliasFoundExceptionMessage = 'Ungültiges {0}-Alias gefunden: {1}' + scheduleDoesNotExistExceptionMessage = "Aufgabenplaner '{0}' existiert nicht." + accessMethodNotExistExceptionMessage = 'Zugriffsmethode nicht vorhanden: {0}.' + oauth2ProviderDoesNotSupportCodeResponseTypeExceptionMessage = "Der OAuth2-Anbieter unterstützt den 'code'-Antworttyp nicht." + untestedPowerShellVersionWarningMessage = '[WARNUNG] Pode {0} wurde nicht auf PowerShell {1} getestet, da diese Version bei der Veröffentlichung von Pode nicht verfügbar war.' + secretVaultAlreadyRegisteredAutoImportExceptionMessage = "Ein Geheimtresor mit dem Namen '{0}' wurde bereits beim automatischen Importieren von Geheimtresoren registriert." + schemeRequiresValidScriptBlockExceptionMessage = "Das bereitgestellte Schema für den Authentifizierungsvalidator '{0}' erfordert einen gültigen ScriptBlock." + serverLoopingMessage = 'Server-Schleife alle {0} Sekunden' + certificateThumbprintsNameSupportedOnWindowsExceptionMessage = 'Zertifikat-Thumbprints/Name werden nur unter Windows unterstützt.' + sseConnectionNameRequiredExceptionMessage = "Ein SSE-Verbindungsname ist erforderlich, entweder von -Name oder `$WebEvent.Sse.Namee" + invalidMiddlewareTypeExceptionMessage = 'Eines der angegebenen Middleware-Objekte ist ein ungültiger Typ. Erwartet wurde entweder ein ScriptBlock oder ein Hashtable, aber erhalten wurde: {0}.' + noSecretForJwtSignatureExceptionMessage = 'Es wurde kein Geheimnis für die JWT-Signatur angegeben.' + modulePathDoesNotExistExceptionMessage = 'Der Modulpfad existiert nicht: {0}' + taskAlreadyDefinedExceptionMessage = '[Aufgabe] {0}: Aufgabe bereits definiert.' + verbAlreadyDefinedExceptionMessage = '[Verb] {0}: Bereits definiert.' + clientCertificatesOnlySupportedOnHttpsEndpointsExceptionMessage = 'Clientzertifikate werden nur auf HTTPS-Endpunkten unterstützt.' + endpointNameNotExistExceptionMessage = "Der Endpunkt mit dem Namen '{0}' existiert nicht" + middlewareNoLogicSuppliedExceptionMessage = '[Middleware]: Kein Logik-ScriptBlock bereitgestellt.' + scriptBlockRequiredForMergingUsersExceptionMessage = 'Ein ScriptBlock ist erforderlich, um mehrere authentifizierte Benutzer zu einem Objekt zusammenzuführen, wenn Valid All ist.' + secretVaultAlreadyRegisteredExceptionMessage = "Ein Geheimnis-Tresor mit dem Namen '{0}' wurde bereits registriert{1}." + deprecatedTitleVersionDescriptionWarningMessage = "WARNUNG: Titel, Version und Beschreibung in 'Enable-PodeOpenApi' sind veraltet. Bitte verwenden Sie stattdessen 'Add-PodeOAInfo'." + undefinedOpenApiReferencesMessage = 'Nicht definierte OpenAPI-Referenzen:' + doneMessage = 'Fertig' + swaggerEditorDoesNotSupportOpenApi31ExceptionMessage = 'Diese Version des Swagger-Editors unterstützt OpenAPI 3.1 nicht.' + durationMustBeZeroOrGreaterExceptionMessage = 'Die Dauer muss 0 oder größer sein, aber erhalten: {0}s' + viewsPathDoesNotExistExceptionMessage = 'Der Ansichtsordnerpfad existiert nicht: {0}' + discriminatorIncompatibleWithAllOfExceptionMessage = "Der Parameter 'Discriminator' ist nicht mit 'allOf' kompatibel." + noNameForWebSocketSendMessageExceptionMessage = 'Kein Name für das Senden einer Nachricht an den WebSocket angegeben.' + hashtableMiddlewareNoLogicExceptionMessage = 'Eine angegebene Hashtable-Middleware enthält keine definierte Logik.' + openApiInfoMessage = 'OpenAPI-Informationen:' + invalidSchemeForAuthValidatorExceptionMessage = "Das bereitgestellte '{0}'-Schema für den Authentifizierungsvalidator '{1}' erfordert einen gültigen ScriptBlock." + sseFailedToBroadcastExceptionMessage = 'SSE konnte aufgrund des definierten SSE-Broadcast-Levels für {0}: {1} nicht übertragen werden.' + adModuleWindowsOnlyExceptionMessage = 'Active Directory-Modul nur unter Windows verfügbar.' + requestLoggingAlreadyEnabledExceptionMessage = 'Die Anforderungsprotokollierung wurde bereits aktiviert.' + invalidAccessControlMaxAgeDurationExceptionMessage = 'Ungültige Access-Control-Max-Age-Dauer angegeben: {0}. Sollte größer als 0 sein.' + openApiDefinitionAlreadyExistsExceptionMessage = 'Die OpenAPI-Definition mit dem Namen {0} existiert bereits.' + renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag kann nicht innerhalb eines 'ScriptBlock' von Select-PodeOADefinition verwendet werden." + taskProcessDoesNotExistExceptionMessage = "Der Aufgabenprozess '{0}' existiert nicht." + scheduleProcessDoesNotExistExceptionMessage = "Der Aufgabenplanerprozess '{0}' existiert nicht." + definitionTagChangeNotAllowedExceptionMessage = 'Definitionstag für eine Route kann nicht geändert werden.' + getRequestBodyNotAllowedExceptionMessage = '{0}-Operationen können keinen Anforderungstext haben.' + fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "Die Funktion '{0}' akzeptiert kein Array als Pipeline-Eingabe." + unsupportedStreamCompressionEncodingExceptionMessage = 'Die Stream-Komprimierungskodierung wird nicht unterstützt: {0}' +} \ No newline at end of file diff --git a/src/Locales/en-us/Pode.psd1 b/src/Locales/en-us/Pode.psd1 new file mode 100644 index 000000000..85ef1c3a5 --- /dev/null +++ b/src/Locales/en-us/Pode.psd1 @@ -0,0 +1,294 @@ +@{ + schemaValidationRequiresPowerShell610ExceptionMessage = 'Schema validation requires PowerShell version 6.1.0 or greater.' + customAccessPathOrScriptBlockRequiredExceptionMessage = 'A Path or ScriptBlock is required for sourcing the Custom access values.' + operationIdMustBeUniqueForArrayExceptionMessage = 'OperationID: {0} has to be unique and cannot be applied to an array.' + endpointNotDefinedForRedirectingExceptionMessage = "An endpoint named '{0}' has not been defined for redirecting." + filesHaveChangedMessage = 'The following files have changed:' + iisAspnetcoreTokenMissingExceptionMessage = 'IIS ASPNETCORE_TOKEN is missing.' + minValueGreaterThanMaxExceptionMessage = 'Min value for {0} should not be greater than the max value.' + noLogicPassedForRouteExceptionMessage = 'No logic passed for Route: {0}' + scriptPathDoesNotExistExceptionMessage = 'The script path does not exist: {0}' + mutexAlreadyExistsExceptionMessage = 'A mutex with the following name already exists: {0}' + listeningOnEndpointsMessage = 'Listening on the following {0} endpoint(s) [{1} thread(s)]:' + unsupportedFunctionInServerlessContextExceptionMessage = 'The {0} function is not supported in a serverless context.' + expectedNoJwtSignatureSuppliedExceptionMessage = 'Expected no JWT signature to be supplied.' + secretAlreadyMountedExceptionMessage = "A Secret with the name '{0}' has already been mounted." + failedToAcquireLockExceptionMessage = 'Failed to acquire a lock on the object.' + noPathSuppliedForStaticRouteExceptionMessage = '[{0}]: No Path supplied for Static Route.' + invalidHostnameSuppliedExceptionMessage = 'Invalid hostname supplied: {0}' + authMethodAlreadyDefinedExceptionMessage = 'Authentication method already defined: {0}' + csrfCookieRequiresSecretExceptionMessage = "When using cookies for CSRF, a Secret is required. You can either supply a Secret or set the Cookie global secret - (Set-PodeCookieSecret '' -Global)" + nonEmptyScriptBlockRequiredForPageRouteExceptionMessage = 'A non-empty ScriptBlock is required to create a Page Route.' + noPropertiesMutuallyExclusiveExceptionMessage = "The parameter 'NoProperties' is mutually exclusive with 'Properties', 'MinProperties' and 'MaxProperties'" + incompatiblePodeDllExceptionMessage = 'An existing incompatible Pode.DLL version {0} is loaded. Version {1} is required. Open a new Powershell/pwsh session and retry.' + accessMethodDoesNotExistExceptionMessage = 'Access method does not exist: {0}.' + scheduleAlreadyDefinedExceptionMessage = '[Schedule] {0}: Schedule already defined.' + secondsValueCannotBeZeroOrLessExceptionMessage = 'Seconds value cannot be 0 or less for {0}' + pathToLoadNotFoundExceptionMessage = 'Path to load {0} not found: {1}' + failedToImportModuleExceptionMessage = 'Failed to import module: {0}' + endpointNotExistExceptionMessage = "Endpoint with protocol '{0}' and address '{1}' or local address '{2}' does not exist." + terminatingMessage = 'Terminating...' + noCommandsSuppliedToConvertToRoutesExceptionMessage = 'No commands supplied to convert to Routes.' + invalidTaskTypeExceptionMessage = 'Task type is invalid, expected either [System.Threading.Tasks.Task] or [hashtable]' + alreadyConnectedToWebSocketExceptionMessage = "Already connected to WebSocket with name '{0}'" + crlfMessageEndCheckOnlySupportedOnTcpEndpointsExceptionMessage = 'The CRLF message end check is only supported on TCP endpoints.' + testPodeOAComponentSchemaNeedToBeEnabledExceptionMessage = "'Test-PodeOAComponentschema' need to be enabled using 'Enable-PodeOpenApi -EnableSchemaValidation'" + adModuleNotInstalledExceptionMessage = 'Active Directory module is not installed.' + cronExpressionInvalidExceptionMessage = 'Cron expression should only consist of 5 parts: {0}' + noSessionToSetOnResponseExceptionMessage = 'There is no session available to set on the response.' + valueOutOfRangeExceptionMessage = "Value '{0}' for {1} is invalid, should be between {2} and {3}" + loggingMethodAlreadyDefinedExceptionMessage = 'Logging method already defined: {0}' + noSecretForHmac256ExceptionMessage = 'No secret supplied for HMAC256 hash.' + eolPowerShellWarningMessage = '[WARNING] Pode {0} has not been tested on PowerShell {1}, as it is EOL.' + runspacePoolFailedToLoadExceptionMessage = '{0} RunspacePool failed to load.' + noEventRegisteredExceptionMessage = 'No {0} event registered: {1}' + scheduleCannotHaveNegativeLimitExceptionMessage = '[Schedule] {0}: Cannot have a negative limit.' + openApiRequestStyleInvalidForParameterExceptionMessage = 'OpenApi request Style cannot be {0} for a {1} parameter.' + openApiDocumentNotCompliantExceptionMessage = 'OpenAPI document is not compliant.' + taskDoesNotExistExceptionMessage = "Task '{0}' does not exist." + scopedVariableNotFoundExceptionMessage = 'Scoped Variable not found: {0}' + sessionsRequiredForCsrfExceptionMessage = 'Sessions are required to use CSRF unless you want to use cookies.' + nonEmptyScriptBlockRequiredForLoggingMethodExceptionMessage = 'A non-empty ScriptBlock is required for the logging method.' + credentialsPassedWildcardForHeadersLiteralExceptionMessage = 'When Credentials is passed, The * wildcard for Headers will be taken as a literal string and not a wildcard.' + podeNotInitializedExceptionMessage = 'Pode has not been initialized.' + multipleEndpointsForGuiMessage = 'Multiple endpoints defined, only the first will be used for the GUI.' + operationIdMustBeUniqueExceptionMessage = 'OperationID: {0} has to be unique.' + invalidJsonJwtExceptionMessage = 'Invalid JSON value found in JWT' + noAlgorithmInJwtHeaderExceptionMessage = 'No algorithm supplied in JWT Header.' + openApiVersionPropertyMandatoryExceptionMessage = 'OpenApi Version property is mandatory.' + limitValueCannotBeZeroOrLessExceptionMessage = 'Limit value cannot be 0 or less for {0}' + timerDoesNotExistExceptionMessage = "Timer '{0}' does not exist." + openApiGenerationDocumentErrorMessage = 'OpenAPI generation document error:' + routeAlreadyContainsCustomAccessExceptionMessage = "Route '[{0}] {1}' already contains Custom Access with name '{2}'" + maximumConcurrentWebSocketThreadsLessThanMinimumExceptionMessage = 'Maximum concurrent WebSocket threads cannot be less than the minimum of {0} but got: {1}' + middlewareAlreadyDefinedExceptionMessage = '[Middleware] {0}: Middleware already defined.' + invalidAtomCharacterExceptionMessage = 'Invalid atom character: {0}' + invalidCronAtomFormatExceptionMessage = 'Invalid cron atom format found: {0}' + cacheStorageNotFoundForRetrieveExceptionMessage = "Cache storage with name '{0}' not found when attempting to retrieve cached item '{1}'" + headerMustHaveNameInEncodingContextExceptionMessage = 'Header must have a name when used in an encoding context.' + moduleDoesNotContainFunctionExceptionMessage = 'Module {0} does not contain function {1} to convert to a Route.' + pathToIconForGuiDoesNotExistExceptionMessage = 'Path to the icon for GUI does not exist: {0}' + noTitleSuppliedForPageExceptionMessage = 'No title supplied for {0} page.' + certificateSuppliedForNonHttpsWssEndpointExceptionMessage = 'Certificate supplied for non-HTTPS/WSS endpoint.' + cannotLockNullObjectExceptionMessage = 'Cannot lock an object that is null.' + showPodeGuiOnlyAvailableOnWindowsExceptionMessage = 'Show-PodeGui is currently only available for Windows PowerShell and PowerShell 7+ on Windows OS.' + unlockSecretButNoScriptBlockExceptionMessage = 'Unlock secret supplied for custom Secret Vault type, but not Unlock ScriptBlock supplied.' + invalidIpAddressExceptionMessage = 'The IP address supplied is invalid: {0}' + maxDaysInvalidExceptionMessage = 'MaxDays must be 0 or greater, but got: {0}' + noRemoveScriptBlockForVaultExceptionMessage = "No Remove ScriptBlock supplied for removing secrets from the vault '{0}'" + noSecretExpectedForNoSignatureExceptionMessage = 'Expected no secret to be supplied for no signature.' + noCertificateFoundExceptionMessage = "No certificate could be found in {0}{1} for '{2}'" + minValueInvalidExceptionMessage = "Min value '{0}' for {1} is invalid, should be greater than/equal to {2}" + accessRequiresAuthenticationOnRoutesExceptionMessage = 'Access requires Authentication to be supplied on Routes.' + noSecretForHmac384ExceptionMessage = 'No secret supplied for HMAC384 hash.' + windowsLocalAuthSupportIsForWindowsOnlyExceptionMessage = 'Windows Local Authentication support is for Windows OS only.' + definitionTagNotDefinedExceptionMessage = 'DefinitionTag {0} does not exist.' + noComponentInDefinitionExceptionMessage = 'No component of type {0} named {1} is available in the {2} definition.' + noSmtpHandlersDefinedExceptionMessage = 'No SMTP handlers have been defined.' + sessionMiddlewareAlreadyInitializedExceptionMessage = 'Session Middleware has already been initialized.' + reusableComponentPathItemsNotAvailableInOpenApi30ExceptionMessage = "The 'pathItems' reusable component feature is not available in OpenAPI v3.0." + wildcardHeadersIncompatibleWithAutoHeadersExceptionMessage = 'The * wildcard for Headers is incompatible with the AutoHeaders switch.' + noDataForFileUploadedExceptionMessage = "No data for file '{0}' was uploaded in the request." + sseOnlyConfiguredOnEventStreamAcceptHeaderExceptionMessage = 'SSE can only be configured on requests with an Accept header value of text/event-stream' + noSessionAvailableToSaveExceptionMessage = 'There is no session available to save.' + pathParameterRequiresRequiredSwitchExceptionMessage = "If the parameter location is 'Path', the switch parameter 'Required' is mandatory." + noOpenApiUrlSuppliedExceptionMessage = 'No OpenAPI URL supplied for {0}.' + maximumConcurrentSchedulesInvalidExceptionMessage = 'Maximum concurrent schedules must be >=1 but got: {0}' + snapinsSupportedOnWindowsPowershellOnlyExceptionMessage = 'Snapins are only supported on Windows PowerShell.' + eventViewerLoggingSupportedOnWindowsOnlyExceptionMessage = 'Event Viewer logging only supported on Windows OS.' + parametersMutuallyExclusiveExceptionMessage = "Parameters '{0}' and '{1}' are mutually exclusive." + pathItemsFeatureNotSupportedInOpenApi30ExceptionMessage = 'The PathItems feature is not supported in OpenAPI v3.0.x' + openApiParameterRequiresNameExceptionMessage = 'The OpenApi parameter requires a name to be specified.' + maximumConcurrentTasksLessThanMinimumExceptionMessage = 'Maximum concurrent tasks cannot be less than the minimum of {0} but got: {1}' + noSemaphoreFoundExceptionMessage = "No semaphore found called '{0}'" + singleValueForIntervalExceptionMessage = 'You can only supply a single {0} value when using intervals.' + jwtNotYetValidExceptionMessage = 'The JWT is not yet valid for use.' + verbAlreadyDefinedForUrlExceptionMessage = '[Verb] {0}: Already defined for {1}' + noSecretNamedMountedExceptionMessage = "No Secret named '{0}' has been mounted." + moduleOrVersionNotFoundExceptionMessage = 'Module or version not found on {0}: {1}@{2}' + noScriptBlockSuppliedExceptionMessage = 'No ScriptBlock supplied.' + noSecretVaultRegisteredExceptionMessage = "No Secret Vault with the name '{0}' has been registered." + nameRequiredForEndpointIfRedirectToSuppliedExceptionMessage = 'A Name is required for the endpoint if the RedirectTo parameter is supplied.' + openApiLicenseObjectRequiresNameExceptionMessage = "The OpenAPI object 'license' required the property 'name'. Use -LicenseName parameter." + sourcePathDoesNotExistForStaticRouteExceptionMessage = '{0}: The Source path supplied for Static Route does not exist: {1}' + noNameForWebSocketDisconnectExceptionMessage = 'No Name for a WebSocket to disconnect from supplied.' + certificateExpiredExceptionMessage = "The certificate '{0}' has expired: {1}" + secretVaultUnlockExpiryDateInPastExceptionMessage = 'Secret Vault unlock expiry date is in the past (UTC): {0}' + invalidWebExceptionTypeExceptionMessage = 'Exception is of an invalid type, should be either WebException or HttpRequestException, but got: {0}' + invalidSecretValueTypeExceptionMessage = 'Secret value is of an invalid type. Expected types: String, SecureString, HashTable, Byte[], or PSCredential. But got: {0}' + explicitTlsModeOnlySupportedOnSmtpsTcpsEndpointsExceptionMessage = 'The Explicit TLS mode is only supported on SMTPS and TCPS endpoints.' + discriminatorMappingRequiresDiscriminatorPropertyExceptionMessage = "The parameter 'DiscriminatorMapping' can only be used when 'DiscriminatorProperty' is present." + scriptErrorExceptionMessage = "Error '{0}' in script {1} {2} (line {3}) char {4} executing {5} on {6} object '{7}' Class: {8} BaseClass: {9}" + cannotSupplyIntervalForQuarterExceptionMessage = 'Cannot supply interval value for every quarter.' + scheduleEndTimeMustBeInFutureExceptionMessage = '[Schedule] {0}: The EndTime value must be in the future.' + invalidJwtSignatureSuppliedExceptionMessage = 'Invalid JWT signature supplied.' + noSetScriptBlockForVaultExceptionMessage = "No Set ScriptBlock supplied for updating/creating secrets in the vault '{0}'" + accessMethodNotExistForMergingExceptionMessage = 'Access method does not exist for merging: {0}' + defaultAuthNotInListExceptionMessage = "The Default Authentication '{0}' is not in the Authentication list supplied." + parameterHasNoNameExceptionMessage = "The Parameter has no name. Please give this component a name using the 'Name' parameter." + methodPathAlreadyDefinedForUrlExceptionMessage = '[{0}] {1}: Already defined for {2}' + fileWatcherAlreadyDefinedExceptionMessage = "A File Watcher named '{0}' has already been defined." + noServiceHandlersDefinedExceptionMessage = 'No Service handlers have been defined.' + secretRequiredForCustomSessionStorageExceptionMessage = 'A Secret is required when using custom session storage.' + secretManagementModuleNotInstalledExceptionMessage = 'Microsoft.PowerShell.SecretManagement module not installed.' + noPathSuppliedForRouteExceptionMessage = 'No Path supplied for the Route.' + validationOfAnyOfSchemaNotSupportedExceptionMessage = "Validation of a schema that includes 'anyof' is not supported." + iisAuthSupportIsForWindowsOnlyExceptionMessage = 'IIS Authentication support is for Windows OS only.' + oauth2InnerSchemeInvalidExceptionMessage = 'OAuth2 InnerScheme can only be one of either Basic or Form authentication, but got: {0}' + noRoutePathSuppliedForPageExceptionMessage = 'No route path supplied for {0} page.' + cacheStorageNotFoundForExistsExceptionMessage = "Cache storage with name '{0}' not found when attempting to check if cached item '{1}' exists." + handlerAlreadyDefinedExceptionMessage = '[{0}] {1}: Handler already defined.' + sessionsNotConfiguredExceptionMessage = 'Sessions have not been configured.' + propertiesTypeObjectAssociationExceptionMessage = 'Only properties of type Object can be associated with {0}.' + sessionsRequiredForSessionPersistentAuthExceptionMessage = 'Sessions are required to use session persistent authentication.' + invalidPathWildcardOrDirectoryExceptionMessage = 'The Path supplied cannot be a wildcard or a directory: {0}' + accessMethodAlreadyDefinedExceptionMessage = 'Access method already defined: {0}' + parametersValueOrExternalValueMandatoryExceptionMessage = "Parameters 'Value' or 'ExternalValue' are mandatory" + maximumConcurrentTasksInvalidExceptionMessage = 'Maximum concurrent tasks must be >=1 but got: {0}' + cannotCreatePropertyWithoutTypeExceptionMessage = 'Cannot create the property because no type is defined.' + authMethodNotExistForMergingExceptionMessage = 'Authentication method does not exist for merging: {0}' + maxValueInvalidExceptionMessage = "Max value '{0}' for {1} is invalid, should be less than/equal to {2}" + endpointAlreadyDefinedExceptionMessage = "An endpoint named '{0}' has already been defined." + eventAlreadyRegisteredExceptionMessage = '{0} event already registered: {1}' + parameterNotSuppliedInRequestExceptionMessage = "A parameter called '{0}' was not supplied in the request or has no data available." + cacheStorageNotFoundForSetExceptionMessage = "Cache storage with name '{0}' not found when attempting to set cached item '{1}'" + methodPathAlreadyDefinedExceptionMessage = '[{0}] {1}: Already defined.' + errorLoggingAlreadyEnabledExceptionMessage = 'Error Logging has already been enabled.' + valueForUsingVariableNotFoundExceptionMessage = "Value for '`$using:{0}' could not be found." + rapidPdfDoesNotSupportOpenApi31ExceptionMessage = "The Document tool RapidPdf doesn't support OpenAPI 3.1" + oauth2ClientSecretRequiredExceptionMessage = 'OAuth2 requires a Client Secret when not using PKCE.' + invalidBase64JwtExceptionMessage = 'Invalid Base64 encoded value found in JWT' + noSessionToCalculateDataHashExceptionMessage = 'No session available to calculate data hash.' + cacheStorageNotFoundForRemoveExceptionMessage = "Cache storage with name '{0}' not found when attempting to remove cached item '{1}'" + csrfMiddlewareNotInitializedExceptionMessage = 'CSRF Middleware has not been initialized.' + infoTitleMandatoryMessage = 'info.title is mandatory.' + typeCanOnlyBeAssociatedWithObjectExceptionMessage = 'Type {0} can only be associated with an Object.' + userFileDoesNotExistExceptionMessage = 'The user file does not exist: {0}' + routeParameterNeedsValidScriptblockExceptionMessage = 'The Route parameter needs a valid, not empty, scriptblock.' + nextTriggerCalculationErrorExceptionMessage = 'Looks like something went wrong trying to calculate the next trigger datetime: {0}' + cannotLockValueTypeExceptionMessage = 'Cannot lock a [ValueType]' + failedToCreateOpenSslCertExceptionMessage = 'Failed to create OpenSSL cert: {0}' + jwtExpiredExceptionMessage = 'The JWT has expired.' + openingGuiMessage = 'Opening the GUI.' + multiTypePropertiesRequireOpenApi31ExceptionMessage = 'Multi-type properties require OpenApi Version 3.1 or above.' + noNameForWebSocketRemoveExceptionMessage = 'No Name for a WebSocket to remove supplied.' + maxSizeInvalidExceptionMessage = 'MaxSize must be 0 or greater, but got: {0}' + iisShutdownMessage = '(IIS Shutdown)' + cannotUnlockValueTypeExceptionMessage = 'Cannot unlock a [ValueType]' + noJwtSignatureForAlgorithmExceptionMessage = 'No JWT signature supplied for {0}.' + maximumConcurrentWebSocketThreadsInvalidExceptionMessage = 'Maximum concurrent WebSocket threads must be >=1 but got: {0}' + acknowledgeMessageOnlySupportedOnSmtpTcpEndpointsExceptionMessage = 'The Acknowledge message is only supported on SMTP and TCP endpoints.' + failedToConnectToUrlExceptionMessage = 'Failed to connect to URL: {0}' + failedToAcquireMutexOwnershipExceptionMessage = 'Failed to acquire mutex ownership. Mutex name: {0}' + sessionsRequiredForOAuth2WithPKCEExceptionMessage = 'Sessions are required to use OAuth2 with PKCE' + failedToConnectToWebSocketExceptionMessage = 'Failed to connect to WebSocket: {0}' + unsupportedObjectExceptionMessage = 'Unsupported object' + failedToParseAddressExceptionMessage = "Failed to parse '{0}' as a valid IP/Host:Port address" + mustBeRunningWithAdminPrivilegesExceptionMessage = 'Must be running with administrator privileges to listen on non-localhost addresses.' + specificationMessage = 'Specification' + cacheStorageNotFoundForClearExceptionMessage = "Cache storage with name '{0}' not found when attempting to clear the cache." + restartingServerMessage = 'Restarting server...' + cannotSupplyIntervalWhenEveryIsNoneExceptionMessage = "Cannot supply an interval when the parameter 'Every' is set to None." + unsupportedJwtAlgorithmExceptionMessage = 'The JWT algorithm is not currently supported: {0}' + websocketsNotConfiguredForSignalMessagesExceptionMessage = 'WebSockets have not been configured to send signal messages.' + invalidLogicTypeInHashtableMiddlewareExceptionMessage = 'A Hashtable Middleware supplied has an invalid Logic type. Expected ScriptBlock, but got: {0}' + maximumConcurrentSchedulesLessThanMinimumExceptionMessage = 'Maximum concurrent schedules cannot be less than the minimum of {0} but got: {1}' + failedToAcquireSemaphoreOwnershipExceptionMessage = 'Failed to acquire semaphore ownership. Semaphore name: {0}' + propertiesParameterWithoutNameExceptionMessage = 'The Properties parameters cannot be used if the Property has no name.' + customSessionStorageMethodNotImplementedExceptionMessage = "The custom session storage does not implement the required '{0}()' method." + authenticationMethodDoesNotExistExceptionMessage = 'Authentication method does not exist: {0}' + webhooksFeatureNotSupportedInOpenApi30ExceptionMessage = 'The Webhooks feature is not supported in OpenAPI v3.0.x' + invalidContentTypeForSchemaExceptionMessage = "Invalid 'content-type' found for schema: {0}" + noUnlockScriptBlockForVaultExceptionMessage = "No Unlock ScriptBlock supplied for unlocking the vault '{0}'" + definitionTagMessage = 'Definition {0}:' + failedToOpenRunspacePoolExceptionMessage = 'Failed to open RunspacePool: {0}' + failedToCloseRunspacePoolExceptionMessage = 'Failed to close RunspacePool: {0}' + verbNoLogicPassedExceptionMessage = '[Verb] {0}: No logic passed' + noMutexFoundExceptionMessage = "No mutex found called '{0}'" + documentationMessage = 'Documentation' + timerAlreadyDefinedExceptionMessage = '[Timer] {0}: Timer already defined.' + invalidPortExceptionMessage = 'The port cannot be negative: {0}' + viewsFolderNameAlreadyExistsExceptionMessage = 'The Views folder name already exists: {0}' + noNameForWebSocketResetExceptionMessage = 'No Name for a WebSocket to reset supplied.' + mergeDefaultAuthNotInListExceptionMessage = "The MergeDefault Authentication '{0}' is not in the Authentication list supplied." + descriptionRequiredExceptionMessage = 'A Description is required for Path:{0} Response:{1}' + pageNameShouldBeAlphaNumericExceptionMessage = 'The Page name should be a valid Alphanumeric value: {0}' + defaultValueNotBooleanOrEnumExceptionMessage = 'The default value is not a boolean and is not part of the enum.' + openApiComponentSchemaDoesNotExistExceptionMessage = "The OpenApi component schema {0} doesn't exist." + timerParameterMustBeGreaterThanZeroExceptionMessage = '[Timer] {0}: {1} must be greater than 0.' + taskTimedOutExceptionMessage = 'Task has timed out after {0}ms.' + scheduleStartTimeAfterEndTimeExceptionMessage = '[Schedule] {0}: Cannot have a StartTime after the EndTime' + infoVersionMandatoryMessage = 'info.version is mandatory.' + cannotUnlockNullObjectExceptionMessage = 'Cannot unlock an object that is null.' + nonEmptyScriptBlockRequiredForCustomAuthExceptionMessage = 'A non-empty ScriptBlock is required for the Custom authentication scheme.' + nonEmptyScriptBlockRequiredForAuthMethodExceptionMessage = 'A non-empty ScriptBlock is required for the authentication method.' + validationOfOneOfSchemaNotSupportedExceptionMessage = "Validation of a schema that includes 'oneof' is not supported." + routeParameterCannotBeNullExceptionMessage = "The parameter 'Route' cannot be null." + cacheStorageAlreadyExistsExceptionMessage = "Cache Storage with name '{0}' already exists." + loggingMethodRequiresValidScriptBlockExceptionMessage = "The supplied output Method for the '{0}' Logging method requires a valid ScriptBlock." + scopedVariableAlreadyDefinedExceptionMessage = 'Scoped Variable already defined: {0}' + oauth2RequiresAuthorizeUrlExceptionMessage = "OAuth2 requires an 'AuthoriseUrl' property to be supplied." + pathNotExistExceptionMessage = 'Path does not exist: {0}' + noDomainServerNameForWindowsAdAuthExceptionMessage = 'No domain server name has been supplied for Windows AD authentication' + suppliedDateAfterScheduleEndTimeExceptionMessage = 'Supplied date is after the end time of the schedule at {0}' + wildcardMethodsIncompatibleWithAutoMethodsExceptionMessage = 'The * wildcard for Methods is incompatible with the AutoMethods switch.' + cannotSupplyIntervalForYearExceptionMessage = 'Cannot supply interval value for every year.' + missingComponentsMessage = 'Missing component(s)' + invalidStrictTransportSecurityDurationExceptionMessage = 'Invalid Strict-Transport-Security duration supplied: {0}. It should be greater than 0.' + noSecretForHmac512ExceptionMessage = 'No secret supplied for HMAC512 hash.' + daysInMonthExceededExceptionMessage = '{0} only has {1} days, but {2} was supplied.' + nonEmptyScriptBlockRequiredForCustomLoggingExceptionMessage = 'A non-empty ScriptBlock is required for the Custom logging output method.' + encodingAttributeOnlyAppliesToMultipartExceptionMessage = 'The encoding attribute only applies to multipart and application/x-www-form-urlencoded request bodies.' + suppliedDateBeforeScheduleStartTimeExceptionMessage = 'Supplied date is before the start time of the schedule at {0}' + unlockSecretRequiredExceptionMessage = "An 'UnlockSecret' property is required when using Microsoft.PowerShell.SecretStore" + noLogicPassedForMethodRouteExceptionMessage = '[{0}] {1}: No logic passed.' + bodyParserAlreadyDefinedForContentTypeExceptionMessage = 'A body-parser is already defined for the {0} content-type.' + invalidJwtSuppliedExceptionMessage = 'Invalid JWT supplied.' + sessionsRequiredForFlashMessagesExceptionMessage = 'Sessions are required to use Flash messages.' + semaphoreAlreadyExistsExceptionMessage = 'A semaphore with the following name already exists: {0}' + invalidJwtHeaderAlgorithmSuppliedExceptionMessage = 'Invalid JWT header algorithm supplied.' + oauth2ProviderDoesNotSupportPasswordGrantTypeExceptionMessage = "The OAuth2 provider does not support the 'password' grant_type required by using an InnerScheme." + invalidAliasFoundExceptionMessage = 'Invalid {0} alias found: {1}' + scheduleDoesNotExistExceptionMessage = "Schedule '{0}' does not exist." + accessMethodNotExistExceptionMessage = 'Access method does not exist: {0}' + oauth2ProviderDoesNotSupportCodeResponseTypeExceptionMessage = "The OAuth2 provider does not support the 'code' response_type." + untestedPowerShellVersionWarningMessage = '[WARNING] Pode {0} has not been tested on PowerShell {1}, as it was not available when Pode was released.' + secretVaultAlreadyRegisteredAutoImportExceptionMessage = "A Secret Vault with the name '{0}' has already been registered while auto-importing Secret Vaults." + schemeRequiresValidScriptBlockExceptionMessage = "The supplied scheme for the '{0}' authentication validator requires a valid ScriptBlock." + serverLoopingMessage = 'Server looping every {0}secs' + certificateThumbprintsNameSupportedOnWindowsExceptionMessage = 'Certificate Thumbprints/Name are only supported on Windows OS.' + sseConnectionNameRequiredExceptionMessage = "An SSE connection Name is required, either from -Name or `$WebEvent.Sse.Name" + invalidMiddlewareTypeExceptionMessage = 'One of the Middlewares supplied is an invalid type. Expected either a ScriptBlock or Hashtable, but got: {0}' + noSecretForJwtSignatureExceptionMessage = 'No secret supplied for JWT signature.' + modulePathDoesNotExistExceptionMessage = 'The module path does not exist: {0}' + taskAlreadyDefinedExceptionMessage = '[Task] {0}: Task already defined.' + verbAlreadyDefinedExceptionMessage = '[Verb] {0}: Already defined' + clientCertificatesOnlySupportedOnHttpsEndpointsExceptionMessage = 'Client certificates are only supported on HTTPS endpoints.' + endpointNameNotExistExceptionMessage = "Endpoint with name '{0}' does not exist." + middlewareNoLogicSuppliedExceptionMessage = '[Middleware]: No logic supplied in ScriptBlock.' + scriptBlockRequiredForMergingUsersExceptionMessage = 'A Scriptblock for merging multiple authenticated users into 1 object is required When Valid is All.' + secretVaultAlreadyRegisteredExceptionMessage = "A Secret Vault with the name '{0}' has already been registered{1}." + deprecatedTitleVersionDescriptionWarningMessage = "WARNING: Title, Version, and Description on 'Enable-PodeOpenApi' are deprecated. Please use 'Add-PodeOAInfo' instead." + undefinedOpenApiReferencesMessage = 'Undefined OpenAPI References:' + doneMessage = 'Done' + swaggerEditorDoesNotSupportOpenApi31ExceptionMessage = "This version on Swagger-Editor doesn't support OpenAPI 3.1" + durationMustBeZeroOrGreaterExceptionMessage = 'Duration must be 0 or greater, but got: {0}s' + viewsPathDoesNotExistExceptionMessage = 'The Views path does not exist: {0}' + discriminatorIncompatibleWithAllOfExceptionMessage = "The parameter 'Discriminator' is incompatible with 'allOf'." + noNameForWebSocketSendMessageExceptionMessage = 'No Name for a WebSocket to send message to supplied.' + hashtableMiddlewareNoLogicExceptionMessage = 'A Hashtable Middleware supplied has no Logic defined.' + openApiInfoMessage = 'OpenAPI Info:' + invalidSchemeForAuthValidatorExceptionMessage = "The supplied '{0}' Scheme for the '{1}' authentication validator requires a valid ScriptBlock." + sseFailedToBroadcastExceptionMessage = 'SSE failed to broadcast due to defined SSE broadcast level for {0}: {1}' + adModuleWindowsOnlyExceptionMessage = 'Active Directory module only available on Windows OS.' + requestLoggingAlreadyEnabledExceptionMessage = 'Request Logging has already been enabled.' + invalidAccessControlMaxAgeDurationExceptionMessage = 'Invalid Access-Control-Max-Age duration supplied: {0}. Should be greater than 0.' + openApiDefinitionAlreadyExistsExceptionMessage = 'OpenAPI definition named {0} already exists.' + renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag cannot be used inside a Select-PodeOADefinition 'ScriptBlock'." + taskProcessDoesNotExistExceptionMessage = 'Task process does not exist: {0}' + scheduleProcessDoesNotExistExceptionMessage = 'Schedule process does not exist: {0}' + definitionTagChangeNotAllowedExceptionMessage = 'Definition Tag for a Route cannot be changed.' + getRequestBodyNotAllowedExceptionMessage = '{0} operations cannot have a Request Body.' + fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "The function '{0}' does not accept an array as pipeline input." + unsupportedStreamCompressionEncodingExceptionMessage = 'Unsupported stream compression encoding: {0}' +} \ No newline at end of file diff --git a/src/Locales/en/Pode.psd1 b/src/Locales/en/Pode.psd1 new file mode 100644 index 000000000..4aa2f34f7 --- /dev/null +++ b/src/Locales/en/Pode.psd1 @@ -0,0 +1,294 @@ +@{ + schemaValidationRequiresPowerShell610ExceptionMessage = 'Schema validation requires PowerShell version 6.1.0 or greater.' + customAccessPathOrScriptBlockRequiredExceptionMessage = 'A Path or ScriptBlock is required for sourcing the Custom access values.' + operationIdMustBeUniqueForArrayExceptionMessage = 'OperationID: {0} has to be unique and cannot be applied to an array.' + endpointNotDefinedForRedirectingExceptionMessage = "An endpoint named '{0}' has not been defined for redirecting." + filesHaveChangedMessage = 'The following files have changed:' + iisAspnetcoreTokenMissingExceptionMessage = 'IIS ASPNETCORE_TOKEN is missing.' + minValueGreaterThanMaxExceptionMessage = 'Min value for {0} should not be greater than the max value.' + noLogicPassedForRouteExceptionMessage = 'No logic passed for Route: {0}' + scriptPathDoesNotExistExceptionMessage = 'The script path does not exist: {0}' + mutexAlreadyExistsExceptionMessage = 'A mutex with the following name already exists: {0}' + listeningOnEndpointsMessage = 'Listening on the following {0} endpoint(s) [{1} thread(s)]:' + unsupportedFunctionInServerlessContextExceptionMessage = 'The {0} function is not supported in a serverless context.' + expectedNoJwtSignatureSuppliedExceptionMessage = 'Expected no JWT signature to be supplied.' + secretAlreadyMountedExceptionMessage = "A Secret with the name '{0}' has already been mounted." + failedToAcquireLockExceptionMessage = 'Failed to acquire a lock on the object.' + noPathSuppliedForStaticRouteExceptionMessage = '[{0}]: No Path supplied for Static Route.' + invalidHostnameSuppliedExceptionMessage = 'Invalid hostname supplied: {0}' + authMethodAlreadyDefinedExceptionMessage = 'Authentication method already defined: {0}' + csrfCookieRequiresSecretExceptionMessage = "When using cookies for CSRF, a Secret is required. You can either supply a Secret or set the Cookie global secret - (Set-PodeCookieSecret '' -Global)" + nonEmptyScriptBlockRequiredForAuthMethodExceptionMessage = 'A non-empty ScriptBlock is required for the authentication method.' + nonEmptyScriptBlockRequiredForPageRouteExceptionMessage = 'A non-empty ScriptBlock is required to create a Page Route.' + noPropertiesMutuallyExclusiveExceptionMessage = "The parameter 'NoProperties' is mutually exclusive with 'Properties', 'MinProperties' and 'MaxProperties'" + incompatiblePodeDllExceptionMessage = 'An existing incompatible Pode.DLL version {0} is loaded. Version {1} is required. Open a new PowerShell/pwsh session and retry.' + accessMethodDoesNotExistExceptionMessage = 'Access method does not exist: {0}.' + scheduleAlreadyDefinedExceptionMessage = '[Schedule] {0}: Schedule already defined.' + secondsValueCannotBeZeroOrLessExceptionMessage = 'Seconds value cannot be 0 or less for {0}' + pathToLoadNotFoundExceptionMessage = 'Path to load {0} not found: {1}' + failedToImportModuleExceptionMessage = 'Failed to import module: {0}' + endpointNotExistExceptionMessage = "Endpoint with protocol '{0}' and address '{1}' or local address '{2}' does not exist." + terminatingMessage = 'Terminating...' + noCommandsSuppliedToConvertToRoutesExceptionMessage = 'No commands supplied to convert to Routes.' + invalidTaskTypeExceptionMessage = 'Task type is invalid, expected either [System.Threading.Tasks.Task] or [hashtable]' + alreadyConnectedToWebSocketExceptionMessage = "Already connected to WebSocket with name '{0}'" + crlfMessageEndCheckOnlySupportedOnTcpEndpointsExceptionMessage = 'The CRLF message end check is only supported on TCP endpoints.' + testPodeOAComponentSchemaNeedToBeEnabledExceptionMessage = "'Test-PodeOAComponentSchema' need to be enabled using 'Enable-PodeOpenApi -EnableSchemaValidation'" + adModuleNotInstalledExceptionMessage = 'Active Directory module is not installed.' + cronExpressionInvalidExceptionMessage = 'Cron expression should only consist of 5 parts: {0}' + noSessionToSetOnResponseExceptionMessage = 'There is no session available to set on the response.' + valueOutOfRangeExceptionMessage = "Value '{0}' for {1} is invalid, should be between {2} and {3}" + loggingMethodAlreadyDefinedExceptionMessage = 'Logging method already defined: {0}' + noSecretForHmac256ExceptionMessage = 'No secret supplied for HMAC256 hash.' + eolPowerShellWarningMessage = '[WARNING] Pode {0} has not been tested on PowerShell {1}, as it is EOL.' + runspacePoolFailedToLoadExceptionMessage = '{0} RunspacePool failed to load.' + noEventRegisteredExceptionMessage = 'No {0} event registered: {1}' + scheduleCannotHaveNegativeLimitExceptionMessage = '[Schedule] {0}: Cannot have a negative limit.' + openApiRequestStyleInvalidForParameterExceptionMessage = 'OpenApi request Style cannot be {0} for a {1} parameter.' + openApiDocumentNotCompliantExceptionMessage = 'OpenAPI document is not compliant.' + taskDoesNotExistExceptionMessage = "Task '{0}' does not exist." + scopedVariableNotFoundExceptionMessage = 'Scoped Variable not found: {0}' + sessionsRequiredForCsrfExceptionMessage = 'Sessions are required to use CSRF unless you want to use cookies.' + nonEmptyScriptBlockRequiredForLoggingMethodExceptionMessage = 'A non-empty ScriptBlock is required for the logging method.' + credentialsPassedWildcardForHeadersLiteralExceptionMessage = 'When Credentials is passed, The * wildcard for Headers will be taken as a literal string and not a wildcard.' + podeNotInitializedExceptionMessage = 'Pode has not been initialised.' + multipleEndpointsForGuiMessage = 'Multiple endpoints defined, only the first will be used for the GUI.' + operationIdMustBeUniqueExceptionMessage = 'OperationID: {0} has to be unique.' + invalidJsonJwtExceptionMessage = 'Invalid JSON value found in JWT' + noAlgorithmInJwtHeaderExceptionMessage = 'No algorithm supplied in JWT Header.' + openApiVersionPropertyMandatoryExceptionMessage = 'OpenApi Version property is mandatory.' + limitValueCannotBeZeroOrLessExceptionMessage = 'Limit value cannot be 0 or less for {0}' + timerDoesNotExistExceptionMessage = "Timer '{0}' does not exist." + openApiGenerationDocumentErrorMessage = 'OpenAPI generation document error:' + routeAlreadyContainsCustomAccessExceptionMessage = "Route '[{0}] {1}' already contains Custom Access with name '{2}'" + maximumConcurrentWebSocketThreadsLessThanMinimumExceptionMessage = 'Maximum concurrent WebSocket threads cannot be less than the minimum of {0} but got: {1}' + middlewareAlreadyDefinedExceptionMessage = '[Middleware] {0}: Middleware already defined.' + invalidAtomCharacterExceptionMessage = 'Invalid atom character: {0}' + invalidCronAtomFormatExceptionMessage = 'Invalid cron atom format found: {0}' + cacheStorageNotFoundForRetrieveExceptionMessage = "Cache storage with name '{0}' not found when attempting to retrieve cached item '{1}'" + headerMustHaveNameInEncodingContextExceptionMessage = 'Header must have a name when used in an encoding context.' + moduleDoesNotContainFunctionExceptionMessage = 'Module {0} does not contain function {1} to convert to a Route.' + pathToIconForGuiDoesNotExistExceptionMessage = 'Path to the icon for GUI does not exist: {0}' + noTitleSuppliedForPageExceptionMessage = 'No title supplied for {0} page.' + certificateSuppliedForNonHttpsWssEndpointExceptionMessage = 'Certificate supplied for non-HTTPS/WSS endpoint.' + cannotLockNullObjectExceptionMessage = 'Cannot lock an object that is null.' + showPodeGuiOnlyAvailableOnWindowsExceptionMessage = 'Show-PodeGui is currently only available for Windows PowerShell and PowerShell 7+ on Windows OS.' + unlockSecretButNoScriptBlockExceptionMessage = 'Unlock secret supplied for custom Secret Vault type, but not Unlock ScriptBlock supplied.' + invalidIpAddressExceptionMessage = 'The IP address supplied is invalid: {0}' + maxDaysInvalidExceptionMessage = 'MaxDays must be 0 or greater, but got: {0}' + noRemoveScriptBlockForVaultExceptionMessage = "No Remove ScriptBlock supplied for removing secrets from the vault '{0}'" + noSecretExpectedForNoSignatureExceptionMessage = 'Expected no secret to be supplied for no signature.' + noCertificateFoundExceptionMessage = "No certificate could be found in {0}{1} for '{2}'" + minValueInvalidExceptionMessage = "Min value '{0}' for {1} is invalid, should be greater than/equal to {2}" + accessRequiresAuthenticationOnRoutesExceptionMessage = 'Access requires Authentication to be supplied on Routes.' + noSecretForHmac384ExceptionMessage = 'No secret supplied for HMAC384 hash.' + windowsLocalAuthSupportIsForWindowsOnlyExceptionMessage = 'Windows Local Authentication support is for Windows OS only.' + definitionTagNotDefinedExceptionMessage = 'DefinitionTag {0} does not exist.' + noComponentInDefinitionExceptionMessage = 'No component of type {0} named {1} is available in the {2} definition.' + noSmtpHandlersDefinedExceptionMessage = 'No SMTP handlers have been defined.' + sessionMiddlewareAlreadyInitializedExceptionMessage = 'Session Middleware has already been initialised.' + reusableComponentPathItemsNotAvailableInOpenApi30ExceptionMessage = "The 'pathItems' reusable component feature is not available in OpenAPI v3.0." + wildcardHeadersIncompatibleWithAutoHeadersExceptionMessage = 'The * wildcard for Headers is incompatible with the AutoHeaders switch.' + noDataForFileUploadedExceptionMessage = "No data for file '{0}' was uploaded in the request." + sseOnlyConfiguredOnEventStreamAcceptHeaderExceptionMessage = 'SSE can only be configured on requests with an Accept header value of text/event-stream' + noSessionAvailableToSaveExceptionMessage = 'There is no session available to save.' + pathParameterRequiresRequiredSwitchExceptionMessage = "If the parameter location is 'Path', the switch parameter 'Required' is mandatory." + noOpenApiUrlSuppliedExceptionMessage = 'No OpenAPI URL supplied for {0}.' + maximumConcurrentSchedulesInvalidExceptionMessage = 'Maximum concurrent schedules must be >=1 but got: {0}' + snapinsSupportedOnWindowsPowershellOnlyExceptionMessage = 'Snapins are only supported on Windows PowerShell.' + eventViewerLoggingSupportedOnWindowsOnlyExceptionMessage = 'Event Viewer logging only supported on Windows OS.' + parametersMutuallyExclusiveExceptionMessage = "Parameters '{0}' and '{1}' are mutually exclusive." + pathItemsFeatureNotSupportedInOpenApi30ExceptionMessage = 'The PathItems feature is not supported in OpenAPI v3.0.x' + openApiParameterRequiresNameExceptionMessage = 'The OpenApi parameter requires a name to be specified.' + maximumConcurrentTasksLessThanMinimumExceptionMessage = 'Maximum concurrent tasks cannot be less than the minimum of {0} but got: {1}' + noSemaphoreFoundExceptionMessage = "No semaphore found called '{0}'" + singleValueForIntervalExceptionMessage = 'You can only supply a single {0} value when using intervals.' + jwtNotYetValidExceptionMessage = 'The JWT is not yet valid for use.' + verbAlreadyDefinedForUrlExceptionMessage = '[Verb] {0}: Already defined for {1}' + noSecretNamedMountedExceptionMessage = "No Secret named '{0}' has been mounted." + moduleOrVersionNotFoundExceptionMessage = 'Module or version not found on {0}: {1}@{2}' + noScriptBlockSuppliedExceptionMessage = 'No ScriptBlock supplied.' + noSecretVaultRegisteredExceptionMessage = "No Secret Vault with the name '{0}' has been registered." + nameRequiredForEndpointIfRedirectToSuppliedExceptionMessage = 'A Name is required for the endpoint if the RedirectTo parameter is supplied.' + openApiLicenseObjectRequiresNameExceptionMessage = "The OpenAPI object 'license' required the property 'name'. Use -LicenseName parameter." + sourcePathDoesNotExistForStaticRouteExceptionMessage = '{0}: The Source path supplied for Static Route does not exist: {1}' + noNameForWebSocketDisconnectExceptionMessage = 'No Name for a WebSocket to disconnect from supplied.' + certificateExpiredExceptionMessage = "The certificate '{0}' has expired: {1}" + secretVaultUnlockExpiryDateInPastExceptionMessage = 'Secret Vault unlock expiry date is in the past (UTC): {0}' + invalidWebExceptionTypeExceptionMessage = 'Exception is of an invalid type, should be either WebException or HttpRequestException, but got: {0}' + invalidSecretValueTypeExceptionMessage = 'Secret value is of an invalid type. Expected types: String, SecureString, HashTable, Byte[], or PSCredential. But got: {0}' + explicitTlsModeOnlySupportedOnSmtpsTcpsEndpointsExceptionMessage = 'The Explicit TLS mode is only supported on SMTPS and TCPS endpoints.' + discriminatorMappingRequiresDiscriminatorPropertyExceptionMessage = "The parameter 'DiscriminatorMapping' can only be used when 'DiscriminatorProperty' is present." + scriptErrorExceptionMessage = "Error '{0}' in script {1} {2} (line {3}) char {4} executing {5} on {6} object '{7}' Class: {8} BaseClass: {9}" + cannotSupplyIntervalForQuarterExceptionMessage = 'Cannot supply interval value for every quarter.' + scheduleEndTimeMustBeInFutureExceptionMessage = '[Schedule] {0}: The EndTime value must be in the future.' + invalidJwtSignatureSuppliedExceptionMessage = 'Invalid JWT signature supplied.' + noSetScriptBlockForVaultExceptionMessage = "No Set ScriptBlock supplied for updating/creating secrets in the vault '{0}'" + accessMethodNotExistForMergingExceptionMessage = 'Access method does not exist for merging: {0}' + defaultAuthNotInListExceptionMessage = "The Default Authentication '{0}' is not in the Authentication list supplied." + parameterHasNoNameExceptionMessage = "The Parameter has no name. Please give this component a name using the 'Name' parameter." + methodPathAlreadyDefinedForUrlExceptionMessage = '[{0}] {1}: Already defined for {2}' + fileWatcherAlreadyDefinedExceptionMessage = "A File Watcher named '{0}' has already been defined." + noServiceHandlersDefinedExceptionMessage = 'No Service handlers have been defined.' + secretRequiredForCustomSessionStorageExceptionMessage = 'A Secret is required when using custom session storage.' + secretManagementModuleNotInstalledExceptionMessage = 'Microsoft.PowerShell.SecretManagement module not installed.' + noPathSuppliedForRouteExceptionMessage = 'No Path supplied for the Route.' + validationOfAnyOfSchemaNotSupportedExceptionMessage = "Validation of a schema that includes 'anyof' is not supported." + iisAuthSupportIsForWindowsOnlyExceptionMessage = 'IIS Authentication support is for Windows OS only.' + oauth2InnerSchemeInvalidExceptionMessage = 'OAuth2 InnerScheme can only be one of either Basic or Form authentication, but got: {0}' + noRoutePathSuppliedForPageExceptionMessage = 'No route path supplied for {0} page.' + cacheStorageNotFoundForExistsExceptionMessage = "Cache storage with name '{0}' not found when attempting to check if cached item '{1}' exists." + handlerAlreadyDefinedExceptionMessage = '[{0}] {1}: Handler already defined.' + sessionsNotConfiguredExceptionMessage = 'Sessions have not been configured.' + propertiesTypeObjectAssociationExceptionMessage = 'Only properties of type Object can be associated with {0}.' + sessionsRequiredForSessionPersistentAuthExceptionMessage = 'Sessions are required to use session persistent authentication.' + invalidPathWildcardOrDirectoryExceptionMessage = 'The Path supplied cannot be a wildcard or a directory: {0}' + accessMethodAlreadyDefinedExceptionMessage = 'Access method already defined: {0}' + parametersValueOrExternalValueMandatoryExceptionMessage = "Parameters 'Value' or 'ExternalValue' are mandatory" + maximumConcurrentTasksInvalidExceptionMessage = 'Maximum concurrent tasks must be >=1 but got: {0}' + cannotCreatePropertyWithoutTypeExceptionMessage = 'Cannot create the property because no type is defined.' + authMethodNotExistForMergingExceptionMessage = 'Authentication method does not exist for merging: {0}' + maxValueInvalidExceptionMessage = "Max value '{0}' for {1} is invalid, should be less than/equal to {2}" + endpointAlreadyDefinedExceptionMessage = "An endpoint named '{0}' has already been defined." + eventAlreadyRegisteredExceptionMessage = '{0} event already registered: {1}' + parameterNotSuppliedInRequestExceptionMessage = "A parameter called '{0}' was not supplied in the request or has no data available." + cacheStorageNotFoundForSetExceptionMessage = "Cache storage with name '{0}' not found when attempting to set cached item '{1}'" + methodPathAlreadyDefinedExceptionMessage = '[{0}] {1}: Already defined.' + errorLoggingAlreadyEnabledExceptionMessage = 'Error Logging has already been enabled.' + valueForUsingVariableNotFoundExceptionMessage = "Value for '`$using:{0}' could not be found." + rapidPdfDoesNotSupportOpenApi31ExceptionMessage = "The Document tool RapidPdf doesn't support OpenAPI 3.1" + oauth2ClientSecretRequiredExceptionMessage = 'OAuth2 requires a Client Secret when not using PKCE.' + invalidBase64JwtExceptionMessage = 'Invalid Base64 encoded value found in JWT' + noSessionToCalculateDataHashExceptionMessage = 'No session available to calculate data hash.' + cacheStorageNotFoundForRemoveExceptionMessage = "Cache storage with name '{0}' not found when attempting to remove cached item '{1}'" + csrfMiddlewareNotInitializedExceptionMessage = 'CSRF Middleware has not been initialised.' + infoTitleMandatoryMessage = 'info.title is mandatory.' + typeCanOnlyBeAssociatedWithObjectExceptionMessage = 'Type {0} can only be associated with an Object.' + userFileDoesNotExistExceptionMessage = 'The user file does not exist: {0}' + routeParameterNeedsValidScriptblockExceptionMessage = 'The Route parameter needs a valid, not empty, scriptblock.' + nextTriggerCalculationErrorExceptionMessage = 'Looks like something went wrong trying to calculate the next trigger datetime: {0}' + cannotLockValueTypeExceptionMessage = 'Cannot lock a [ValueType]' + failedToCreateOpenSslCertExceptionMessage = 'Failed to create OpenSSL cert: {0}' + jwtExpiredExceptionMessage = 'The JWT has expired.' + openingGuiMessage = 'Opening the GUI.' + multiTypePropertiesRequireOpenApi31ExceptionMessage = 'Multi-type properties require OpenApi Version 3.1 or above.' + noNameForWebSocketRemoveExceptionMessage = 'No Name for a WebSocket to remove supplied.' + maxSizeInvalidExceptionMessage = 'MaxSize must be 0 or greater, but got: {0}' + iisShutdownMessage = '(IIS Shutdown)' + cannotUnlockValueTypeExceptionMessage = 'Cannot unlock a [ValueType]' + noJwtSignatureForAlgorithmExceptionMessage = 'No JWT signature supplied for {0}.' + maximumConcurrentWebSocketThreadsInvalidExceptionMessage = 'Maximum concurrent WebSocket threads must be >=1 but got: {0}' + acknowledgeMessageOnlySupportedOnSmtpTcpEndpointsExceptionMessage = 'The Acknowledge message is only supported on SMTP and TCP endpoints.' + failedToConnectToUrlExceptionMessage = 'Failed to connect to URL: {0}' + failedToAcquireMutexOwnershipExceptionMessage = 'Failed to acquire mutex ownership. Mutex name: {0}' + sessionsRequiredForOAuth2WithPKCEExceptionMessage = 'Sessions are required to use OAuth2 with PKCE' + failedToConnectToWebSocketExceptionMessage = 'Failed to connect to WebSocket: {0}' + unsupportedObjectExceptionMessage = 'Unsupported object' + failedToParseAddressExceptionMessage = "Failed to parse '{0}' as a valid IP/Host:Port address" + mustBeRunningWithAdminPrivilegesExceptionMessage = 'Must be running with administrator privileges to listen on non-localhost addresses.' + specificationMessage = 'Specification' + cacheStorageNotFoundForClearExceptionMessage = "Cache storage with name '{0}' not found when attempting to clear the cache." + restartingServerMessage = 'Restarting server...' + cannotSupplyIntervalWhenEveryIsNoneExceptionMessage = "Cannot supply an interval when the parameter 'Every' is set to None." + unsupportedJwtAlgorithmExceptionMessage = 'The JWT algorithm is not currently supported: {0}' + websocketsNotConfiguredForSignalMessagesExceptionMessage = 'WebSockets have not been configured to send signal messages.' + invalidLogicTypeInHashtableMiddlewareExceptionMessage = 'A Hashtable Middleware supplied has an invalid Logic type. Expected ScriptBlock, but got: {0}' + maximumConcurrentSchedulesLessThanMinimumExceptionMessage = 'Maximum concurrent schedules cannot be less than the minimum of {0} but got: {1}' + failedToAcquireSemaphoreOwnershipExceptionMessage = 'Failed to acquire semaphore ownership. Semaphore name: {0}' + propertiesParameterWithoutNameExceptionMessage = 'The Properties parameters cannot be used if the Property has no name.' + customSessionStorageMethodNotImplementedExceptionMessage = "The custom session storage does not implement the required '{0}()' method." + authenticationMethodDoesNotExistExceptionMessage = 'Authentication method does not exist: {0}' + webhooksFeatureNotSupportedInOpenApi30ExceptionMessage = 'The Webhooks feature is not supported in OpenAPI v3.0.x' + invalidContentTypeForSchemaExceptionMessage = "Invalid 'content-type' found for schema: {0}" + noUnlockScriptBlockForVaultExceptionMessage = "No Unlock ScriptBlock supplied for unlocking the vault '{0}'" + definitionTagMessage = 'Definition {0}:' + failedToOpenRunspacePoolExceptionMessage = 'Failed to open RunspacePool: {0}' + failedToCloseRunspacePoolExceptionMessage = 'Failed to close RunspacePool: {0}' + verbNoLogicPassedExceptionMessage = '[Verb] {0}: No logic passed' + noMutexFoundExceptionMessage = "No mutex found called '{0}'" + documentationMessage = 'Documentation' + timerAlreadyDefinedExceptionMessage = '[Timer] {0}: Timer already defined.' + invalidPortExceptionMessage = 'The port cannot be negative: {0}' + viewsFolderNameAlreadyExistsExceptionMessage = 'The Views folder name already exists: {0}' + noNameForWebSocketResetExceptionMessage = 'No Name for a WebSocket to reset supplied.' + mergeDefaultAuthNotInListExceptionMessage = "The MergeDefault Authentication '{0}' is not in the Authentication list supplied." + descriptionRequiredExceptionMessage = 'A Description is required for Path:{0} Response:{1}' + pageNameShouldBeAlphaNumericExceptionMessage = 'The Page name should be a valid Alphanumeric value: {0}' + defaultValueNotBooleanOrEnumExceptionMessage = 'The default value is not a boolean and is not part of the enum.' + openApiComponentSchemaDoesNotExistExceptionMessage = "The OpenApi component schema {0} doesn't exist." + timerParameterMustBeGreaterThanZeroExceptionMessage = '[Timer] {0}: {1} must be greater than 0.' + taskTimedOutExceptionMessage = 'Task has timed out after {0}ms.' + scheduleStartTimeAfterEndTimeExceptionMessage = '[Schedule] {0}: Cannot have a StartTime after the EndTime' + infoVersionMandatoryMessage = 'info.version is mandatory.' + cannotUnlockNullObjectExceptionMessage = 'Cannot unlock an object that is null.' + nonEmptyScriptBlockRequiredForCustomAuthExceptionMessage = 'A non-empty ScriptBlock is required for the Custom authentication scheme.' + validationOfOneOfSchemaNotSupportedExceptionMessage = "Validation of a schema that includes 'oneof' is not supported." + routeParameterCannotBeNullExceptionMessage = "The parameter 'Route' cannot be null." + cacheStorageAlreadyExistsExceptionMessage = "Cache Storage with name '{0}' already exists." + loggingMethodRequiresValidScriptBlockExceptionMessage = "The supplied output Method for the '{0}' Logging method requires a valid ScriptBlock." + scopedVariableAlreadyDefinedExceptionMessage = 'Scoped Variable already defined: {0}' + oauth2RequiresAuthorizeUrlExceptionMessage = "OAuth2 requires an 'AuthoriseUrl' property to be supplied." + pathNotExistExceptionMessage = 'Path does not exist: {0}' + noDomainServerNameForWindowsAdAuthExceptionMessage = 'No domain server name has been supplied for Windows AD authentication' + suppliedDateAfterScheduleEndTimeExceptionMessage = 'Supplied date is after the end time of the schedule at {0}' + wildcardMethodsIncompatibleWithAutoMethodsExceptionMessage = 'The * wildcard for Methods is incompatible with the AutoMethods switch.' + cannotSupplyIntervalForYearExceptionMessage = 'Cannot supply interval value for every year.' + missingComponentsMessage = 'Missing component(s)' + invalidStrictTransportSecurityDurationExceptionMessage = 'Invalid Strict-Transport-Security duration supplied: {0}. It should be greater than 0.' + noSecretForHmac512ExceptionMessage = 'No secret supplied for HMAC512 hash.' + daysInMonthExceededExceptionMessage = '{0} only has {1} days, but {2} was supplied.' + nonEmptyScriptBlockRequiredForCustomLoggingExceptionMessage = 'A non-empty ScriptBlock is required for the Custom logging output method.' + encodingAttributeOnlyAppliesToMultipartExceptionMessage = 'The encoding attribute only applies to multipart and application/x-www-form-urlencoded request bodies.' + suppliedDateBeforeScheduleStartTimeExceptionMessage = 'Supplied date is before the start time of the schedule at {0}' + unlockSecretRequiredExceptionMessage = "An 'UnlockSecret' property is required when using Microsoft.PowerShell.SecretStore" + noLogicPassedForMethodRouteExceptionMessage = '[{0}] {1}: No logic passed.' + bodyParserAlreadyDefinedForContentTypeExceptionMessage = 'A body-parser is already defined for the {0} content-type.' + invalidJwtSuppliedExceptionMessage = 'Invalid JWT supplied.' + sessionsRequiredForFlashMessagesExceptionMessage = 'Sessions are required to use Flash messages.' + semaphoreAlreadyExistsExceptionMessage = 'A semaphore with the following name already exists: {0}' + invalidJwtHeaderAlgorithmSuppliedExceptionMessage = 'Invalid JWT header algorithm supplied.' + oauth2ProviderDoesNotSupportPasswordGrantTypeExceptionMessage = "The OAuth2 provider does not support the 'password' grant_type required by using an InnerScheme." + invalidAliasFoundExceptionMessage = 'Invalid {0} alias found: {1}' + scheduleDoesNotExistExceptionMessage = "Schedule '{0}' does not exist." + accessMethodNotExistExceptionMessage = 'Access method does not exist: {0}' + oauth2ProviderDoesNotSupportCodeResponseTypeExceptionMessage = "The OAuth2 provider does not support the 'code' response_type." + untestedPowerShellVersionWarningMessage = '[WARNING] Pode {0} has not been tested on PowerShell {1}, as it was not available when Pode was released.' + secretVaultAlreadyRegisteredAutoImportExceptionMessage = "A Secret Vault with the name '{0}' has already been registered while auto-importing Secret Vaults." + schemeRequiresValidScriptBlockExceptionMessage = "The supplied scheme for the '{0}' authentication validator requires a valid ScriptBlock." + serverLoopingMessage = 'Server looping every {0}secs' + certificateThumbprintsNameSupportedOnWindowsExceptionMessage = 'Certificate Thumbprints/Name are only supported on Windows OS.' + sseConnectionNameRequiredExceptionMessage = "An SSE connection Name is required, either from -Name or `$WebEvent.Sse.Name" + invalidMiddlewareTypeExceptionMessage = 'One of the Middlewares supplied is an invalid type. Expected either a ScriptBlock or Hashtable, but got: {0}' + noSecretForJwtSignatureExceptionMessage = 'No secret supplied for JWT signature.' + modulePathDoesNotExistExceptionMessage = 'The module path does not exist: {0}' + taskAlreadyDefinedExceptionMessage = '[Task] {0}: Task already defined.' + verbAlreadyDefinedExceptionMessage = '[Verb] {0}: Already defined' + clientCertificatesOnlySupportedOnHttpsEndpointsExceptionMessage = 'Client certificates are only supported on HTTPS endpoints.' + endpointNameNotExistExceptionMessage = "Endpoint with name '{0}' does not exist." + middlewareNoLogicSuppliedExceptionMessage = '[Middleware]: No logic supplied in ScriptBlock.' + scriptBlockRequiredForMergingUsersExceptionMessage = 'A Scriptblock for merging multiple authenticated users into 1 object is required When Valid is All.' + secretVaultAlreadyRegisteredExceptionMessage = "A Secret Vault with the name '{0}' has already been registered{1}." + deprecatedTitleVersionDescriptionWarningMessage = "WARNING: Title, Version, and Description on 'Enable-PodeOpenApi' are deprecated. Please use 'Add-PodeOAInfo' instead." + undefinedOpenApiReferencesMessage = 'Undefined OpenAPI References:' + doneMessage = 'Done' + swaggerEditorDoesNotSupportOpenApi31ExceptionMessage = "This version on Swagger-Editor doesn't support OpenAPI 3.1" + durationMustBeZeroOrGreaterExceptionMessage = 'Duration must be 0 or greater, but got: {0}s' + viewsPathDoesNotExistExceptionMessage = 'The Views path does not exist: {0}' + discriminatorIncompatibleWithAllOfExceptionMessage = "The parameter 'Discriminator' is incompatible with 'allOf'." + noNameForWebSocketSendMessageExceptionMessage = 'No Name for a WebSocket to send message to supplied.' + hashtableMiddlewareNoLogicExceptionMessage = 'A Hashtable Middleware supplied has no Logic defined.' + openApiInfoMessage = 'OpenAPI Info:' + invalidSchemeForAuthValidatorExceptionMessage = "The supplied '{0}' Scheme for the '{1}' authentication validator requires a valid ScriptBlock." + sseFailedToBroadcastExceptionMessage = 'SSE failed to broadcast due to defined SSE broadcast level for {0}: {1}' + adModuleWindowsOnlyExceptionMessage = 'Active Directory module only available on Windows OS.' + requestLoggingAlreadyEnabledExceptionMessage = 'Request Logging has already been enabled.' + invalidAccessControlMaxAgeDurationExceptionMessage = 'Invalid Access-Control-Max-Age duration supplied: {0}. Should be greater than 0.' + openApiDefinitionAlreadyExistsExceptionMessage = 'OpenAPI definition named {0} already exists.' + renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag cannot be used inside a Select-PodeOADefinition 'ScriptBlock'." + taskProcessDoesNotExistExceptionMessage = 'Task process does not exist: {0}' + scheduleProcessDoesNotExistExceptionMessage = 'Schedule process does not exist: {0}' + definitionTagChangeNotAllowedExceptionMessage = 'Definition Tag for a Route cannot be changed.' + getRequestBodyNotAllowedExceptionMessage = '{0} operations cannot have a Request Body.' + fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "The function '{0}' does not accept an array as pipeline input." + unsupportedStreamCompressionEncodingExceptionMessage = 'Unsupported stream compression encoding: {0}' +} \ No newline at end of file diff --git a/src/Locales/es/Pode.psd1 b/src/Locales/es/Pode.psd1 new file mode 100644 index 000000000..409357fe4 --- /dev/null +++ b/src/Locales/es/Pode.psd1 @@ -0,0 +1,294 @@ +@{ + schemaValidationRequiresPowerShell610ExceptionMessage = 'La validación del esquema requiere PowerShell versión 6.1.0 o superior.' + customAccessPathOrScriptBlockRequiredExceptionMessage = 'Se requiere una ruta o un ScriptBlock para obtener los valores de acceso personalizados.' + operationIdMustBeUniqueForArrayExceptionMessage = 'OperationID: {0} debe ser único y no puede aplicarse a un array.' + endpointNotDefinedForRedirectingExceptionMessage = "No se ha definido un punto de conexión llamado '{0}' para la redirección." + filesHaveChangedMessage = 'Los siguientes archivos han cambiado:' + iisAspnetcoreTokenMissingExceptionMessage = 'Falta el token IIS ASPNETCORE_TOKEN.' + minValueGreaterThanMaxExceptionMessage = 'El valor mínimo para {0} no debe ser mayor que el valor máximo.' + noLogicPassedForRouteExceptionMessage = 'No se pasó lógica para la Ruta: {0}' + scriptPathDoesNotExistExceptionMessage = 'La ruta del script no existe: {0}' + mutexAlreadyExistsExceptionMessage = 'Ya existe un mutex con el siguiente nombre: {0}' + listeningOnEndpointsMessage = 'Escuchando en los siguientes {0} punto(s) de conexión [{1} hilo(s)]:' + unsupportedFunctionInServerlessContextExceptionMessage = 'La función {0} no es compatible en un contexto sin servidor.' + expectedNoJwtSignatureSuppliedExceptionMessage = 'No se esperaba que se proporcionara una firma JWT.' + secretAlreadyMountedExceptionMessage = "Un Secreto con el nombre '{0}' ya ha sido montado." + failedToAcquireLockExceptionMessage = 'No se pudo adquirir un bloqueo en el objeto.' + noPathSuppliedForStaticRouteExceptionMessage = '[{0}]: No se proporcionó una ruta para la Ruta estática.' + invalidHostnameSuppliedExceptionMessage = 'Nombre de host no válido proporcionado: {0}' + authMethodAlreadyDefinedExceptionMessage = 'Método de autenticación ya definido: {0}' + csrfCookieRequiresSecretExceptionMessage = "Al usar cookies para CSRF, se requiere un Secreto. Puedes proporcionar un Secreto o establecer el secreto global de la Cookie - (Set-PodeCookieSecret '' -Global)" + nonEmptyScriptBlockRequiredForPageRouteExceptionMessage = 'Se requiere un ScriptBlock no vacío para crear una Ruta de Página.' + noPropertiesMutuallyExclusiveExceptionMessage = "El parámetro 'NoProperties' es mutuamente excluyente con 'Properties', 'MinProperties' y 'MaxProperties'." + incompatiblePodeDllExceptionMessage = 'Se ha cargado una versión incompatible existente de Pode.DLL {0}. Se requiere la versión {1}. Abra una nueva sesión de Powershell/pwsh e intente de nuevo.' + accessMethodDoesNotExistExceptionMessage = 'El método de acceso no existe: {0}.' + scheduleAlreadyDefinedExceptionMessage = '[Programador] {0}: Programador ya definido.' + secondsValueCannotBeZeroOrLessExceptionMessage = 'El valor en segundos no puede ser 0 o menor para {0}' + pathToLoadNotFoundExceptionMessage = 'No se encontró la ruta para cargar {0}: {1}' + failedToImportModuleExceptionMessage = 'Error al importar el módulo: {0}' + endpointNotExistExceptionMessage = "No existe un punto de conexión con el protocolo '{0}' y la dirección '{1}' o la dirección local '{2}'." + terminatingMessage = 'Terminando...' + noCommandsSuppliedToConvertToRoutesExceptionMessage = 'No se proporcionaron comandos para convertir a Rutas.' + invalidTaskTypeExceptionMessage = 'El tipo de tarea no es válido, se esperaba [System.Threading.Tasks.Task] o [hashtable].' + alreadyConnectedToWebSocketExceptionMessage = "Ya conectado al WebSocket con el nombre '{0}'" + crlfMessageEndCheckOnlySupportedOnTcpEndpointsExceptionMessage = 'La verificación de final de mensaje CRLF solo es compatible con endpoints TCP.' + testPodeOAComponentSchemaNeedToBeEnabledExceptionMessage = "'Test-PodeOAComponentSchema' necesita ser habilitado usando 'Enable-PodeOpenApi -EnableSchemaValidation'" + adModuleNotInstalledExceptionMessage = 'El módulo de Active Directory no está instalado.' + cronExpressionInvalidExceptionMessage = 'La expresión Cron solo debe consistir en 5 partes: {0}' + noSessionToSetOnResponseExceptionMessage = 'No hay ninguna sesión disponible para configurar en la respuesta.' + valueOutOfRangeExceptionMessage = "El valor '{0}' para {1} no es válido, debe estar entre {2} y {3}" + loggingMethodAlreadyDefinedExceptionMessage = 'Método de registro ya definido: {0}' + noSecretForHmac256ExceptionMessage = 'No se suministró ningún secreto para el hash HMAC256.' + eolPowerShellWarningMessage = '[ADVERTENCIA] Pode {0} no se ha probado en PowerShell {1}, ya que está en fin de vida.' + runspacePoolFailedToLoadExceptionMessage = '{0} RunspacePool no se pudo cargar.' + noEventRegisteredExceptionMessage = 'No hay evento {0} registrado: {1}' + scheduleCannotHaveNegativeLimitExceptionMessage = '[Programador] {0}: No puede tener un límite negativo.' + openApiRequestStyleInvalidForParameterExceptionMessage = 'El estilo de la solicitud OpenApi no puede ser {0} para un parámetro {1}.' + openApiDocumentNotCompliantExceptionMessage = 'El documento OpenAPI no cumple con las normas.' + taskDoesNotExistExceptionMessage = "La tarea '{0}' no existe." + scopedVariableNotFoundExceptionMessage = 'Variable de alcance no encontrada: {0}' + sessionsRequiredForCsrfExceptionMessage = 'Se requieren sesiones para usar CSRF a menos que desee usar cookies.' + nonEmptyScriptBlockRequiredForLoggingMethodExceptionMessage = 'Se requiere un ScriptBlock no vacío para el método de registro.' + credentialsPassedWildcardForHeadersLiteralExceptionMessage = 'Cuando se pasan las Credenciales, el comodín * para los Encabezados se tomará como una cadena literal y no como un comodín.' + podeNotInitializedExceptionMessage = 'Pode no se ha inicializado.' + multipleEndpointsForGuiMessage = 'Se han definido múltiples puntos de conexión, solo se usará el primero para la GUI.' + operationIdMustBeUniqueExceptionMessage = 'OperationID: {0} debe ser único.' + invalidJsonJwtExceptionMessage = 'Valor JSON no válido encontrado en JWT' + noAlgorithmInJwtHeaderExceptionMessage = 'No se proporcionó un algoritmo en el encabezado JWT.' + openApiVersionPropertyMandatoryExceptionMessage = 'La propiedad de versión OpenApi es obligatoria.' + limitValueCannotBeZeroOrLessExceptionMessage = 'El valor del límite no puede ser 0 o menor para {0}' + timerDoesNotExistExceptionMessage = "El temporizador '{0}' no existe." + openApiGenerationDocumentErrorMessage = 'Error en el documento de generación de OpenAPI:' + routeAlreadyContainsCustomAccessExceptionMessage = "La ruta '[{0}] {1}' ya contiene acceso personalizado con el nombre '{2}'" + maximumConcurrentWebSocketThreadsLessThanMinimumExceptionMessage = 'El número máximo de hilos concurrentes de WebSocket no puede ser menor que el mínimo de {0}, pero se obtuvo: {1}' + middlewareAlreadyDefinedExceptionMessage = '[Middleware] {0}: Middleware ya definido.' + invalidAtomCharacterExceptionMessage = 'Carácter de átomo cron no válido: {0}' + invalidCronAtomFormatExceptionMessage = 'Formato de átomo cron inválido encontrado: {0}' + cacheStorageNotFoundForRetrieveExceptionMessage = "No se encontró el almacenamiento en caché con el nombre '{0}' al intentar recuperar el elemento en caché '{1}'." + headerMustHaveNameInEncodingContextExceptionMessage = 'El encabezado debe tener un nombre cuando se usa en un contexto de codificación.' + moduleDoesNotContainFunctionExceptionMessage = 'El módulo {0} no contiene la función {1} para convertir en una Ruta.' + pathToIconForGuiDoesNotExistExceptionMessage = 'La ruta del icono para la GUI no existe: {0}' + noTitleSuppliedForPageExceptionMessage = 'No se proporcionó título para la página {0}.' + certificateSuppliedForNonHttpsWssEndpointExceptionMessage = 'Certificado proporcionado para un endpoint que no es HTTPS/WSS.' + cannotLockNullObjectExceptionMessage = 'No se puede bloquear un objeto nulo.' + showPodeGuiOnlyAvailableOnWindowsExceptionMessage = 'Show-PodeGui actualmente solo está disponible para Windows PowerShell y PowerShell 7+ en Windows.' + unlockSecretButNoScriptBlockExceptionMessage = 'Se suministró un secreto de desbloqueo para el tipo de bóveda secreta personalizada, pero no se suministró ningún ScriptBlock de desbloqueo.' + invalidIpAddressExceptionMessage = 'La dirección IP suministrada no es válida: {0}' + maxDaysInvalidExceptionMessage = 'MaxDays debe ser igual o mayor que 0, pero se obtuvo: {0}' + noRemoveScriptBlockForVaultExceptionMessage = "No se suministró ningún ScriptBlock de eliminación para eliminar secretos de la bóveda '{0}'" + noSecretExpectedForNoSignatureExceptionMessage = 'Se esperaba que no se suministrara ningún secreto para ninguna firma.' + noCertificateFoundExceptionMessage = "No se encontró ningún certificado en {0}{1} para '{2}'" + minValueInvalidExceptionMessage = "El valor mínimo '{0}' para {1} no es válido, debe ser mayor o igual a {2}" + accessRequiresAuthenticationOnRoutesExceptionMessage = 'El acceso requiere autenticación en las rutas.' + noSecretForHmac384ExceptionMessage = 'No se suministró ningún secreto para el hash HMAC384.' + windowsLocalAuthSupportIsForWindowsOnlyExceptionMessage = 'El soporte de autenticación local de Windows es solo para Windows.' + definitionTagNotDefinedExceptionMessage = 'La etiqueta de definición {0} no está definida.' + noComponentInDefinitionExceptionMessage = 'No hay componente del tipo {0} llamado {1} disponible en la definición de {2}.' + noSmtpHandlersDefinedExceptionMessage = 'No se han definido controladores SMTP.' + sessionMiddlewareAlreadyInitializedExceptionMessage = 'El Middleware de Sesión ya se ha inicializado.' + reusableComponentPathItemsNotAvailableInOpenApi30ExceptionMessage = "La característica del componente reutilizable 'pathItems' no está disponible en OpenAPI v3.0." + wildcardHeadersIncompatibleWithAutoHeadersExceptionMessage = 'El comodín * para los Encabezados es incompatible con el interruptor AutoHeaders.' + noDataForFileUploadedExceptionMessage = "No se han subido datos para el archivo '{0}' en la solicitud." + sseOnlyConfiguredOnEventStreamAcceptHeaderExceptionMessage = 'SSE solo se puede configurar en solicitudes con un valor de encabezado Accept de text/event-stream.' + noSessionAvailableToSaveExceptionMessage = 'No hay sesión disponible para guardar.' + pathParameterRequiresRequiredSwitchExceptionMessage = "Si la ubicación del parámetro es 'Path', el parámetro switch 'Required' es obligatorio." + noOpenApiUrlSuppliedExceptionMessage = 'No se proporcionó URL de OpenAPI para {0}.' + maximumConcurrentSchedulesInvalidExceptionMessage = 'Las programaciones simultáneos máximos deben ser >=1 pero se obtuvo: {0}' + snapinsSupportedOnWindowsPowershellOnlyExceptionMessage = 'Los Snapins solo son compatibles con Windows PowerShell.' + eventViewerLoggingSupportedOnWindowsOnlyExceptionMessage = 'El registro en el Visor de Eventos solo se admite en Windows.' + parametersMutuallyExclusiveExceptionMessage = "Los parámetros '{0}' y '{1}' son mutuamente excluyentes." + pathItemsFeatureNotSupportedInOpenApi30ExceptionMessage = 'La función de elementos de ruta no es compatible con OpenAPI v3.0.x' + openApiParameterRequiresNameExceptionMessage = 'El parámetro OpenApi requiere un nombre especificado.' + maximumConcurrentTasksLessThanMinimumExceptionMessage = 'El número máximo de tareas concurrentes no puede ser menor que el mínimo de {0}, pero se obtuvo: {1}' + noSemaphoreFoundExceptionMessage = "No se encontró ningún semáforo llamado '{0}'" + singleValueForIntervalExceptionMessage = 'Solo puede suministrar un único valor {0} cuando utiliza intervalos.' + jwtNotYetValidExceptionMessage = 'El JWT aún no es válido.' + verbAlreadyDefinedForUrlExceptionMessage = '[Verbo] {0}: Ya está definido para {1}' + noSecretNamedMountedExceptionMessage = "No se ha montado ningún Secreto con el nombre '{0}'." + moduleOrVersionNotFoundExceptionMessage = 'No se encontró el módulo o la versión en {0}: {1}@{2}' + noScriptBlockSuppliedExceptionMessage = 'No se suministró ningún ScriptBlock.' + noSecretVaultRegisteredExceptionMessage = "No se ha registrado un Cofre de Secretos con el nombre '{0}'." + nameRequiredForEndpointIfRedirectToSuppliedExceptionMessage = 'Se requiere un nombre para el endpoint si se proporciona el parámetro RedirectTo.' + openApiLicenseObjectRequiresNameExceptionMessage = "El objeto OpenAPI 'license' requiere la propiedad 'name'. Use el parámetro -LicenseName." + sourcePathDoesNotExistForStaticRouteExceptionMessage = '{0}: La ruta de origen proporcionada para la Ruta estática no existe: {1}' + noNameForWebSocketDisconnectExceptionMessage = 'No se proporcionó ningún nombre para desconectar el WebSocket.' + certificateExpiredExceptionMessage = "El certificado '{0}' ha expirado: {1}" + secretVaultUnlockExpiryDateInPastExceptionMessage = 'La fecha de expiración para desbloquear el Cofre de Secretos está en el pasado (UTC): {0}' + invalidWebExceptionTypeExceptionMessage = 'La excepción es de un tipo no válido, debe ser WebException o HttpRequestException, pero se obtuvo: {0}' + invalidSecretValueTypeExceptionMessage = 'El valor del secreto es de un tipo no válido. Tipos esperados: String, SecureString, HashTable, Byte[], o PSCredential. Pero se obtuvo: {0}' + explicitTlsModeOnlySupportedOnSmtpsTcpsEndpointsExceptionMessage = 'El modo TLS explícito solo es compatible con endpoints SMTPS y TCPS.' + discriminatorMappingRequiresDiscriminatorPropertyExceptionMessage = "El parámetro 'DiscriminatorMapping' solo se puede usar cuando está presente la propiedad 'DiscriminatorProperty'." + scriptErrorExceptionMessage = "Error '{0}' en el script {1} {2} (línea {3}) carácter {4} al ejecutar {5} en el objeto {6} '{7}' Clase: {8} ClaseBase: {9}" + cannotSupplyIntervalForQuarterExceptionMessage = 'No se puede proporcionar un valor de intervalo para cada trimestre.' + scheduleEndTimeMustBeInFutureExceptionMessage = '[Programador] {0}: El valor de EndTime debe estar en el futuro.' + invalidJwtSignatureSuppliedExceptionMessage = 'Firma JWT proporcionada no válida.' + noSetScriptBlockForVaultExceptionMessage = "No se suministró ningún ScriptBlock de configuración para actualizar/crear secretos en la bóveda '{0}'" + accessMethodNotExistForMergingExceptionMessage = 'El método de acceso no existe para fusionarse: {0}' + defaultAuthNotInListExceptionMessage = "La autenticación predeterminada '{0}' no está en la lista de autenticación proporcionada." + parameterHasNoNameExceptionMessage = "El parámetro no tiene nombre. Asigne un nombre a este componente usando el parámetro 'Name'." + methodPathAlreadyDefinedForUrlExceptionMessage = '[{0}] {1}: Ya está definido para {2}' + fileWatcherAlreadyDefinedExceptionMessage = "Un Observador de Archivos llamado '{0}' ya ha sido definido." + noServiceHandlersDefinedExceptionMessage = 'No se han definido controladores de servicio.' + secretRequiredForCustomSessionStorageExceptionMessage = 'Se requiere un secreto cuando se utiliza el almacenamiento de sesión personalizado.' + secretManagementModuleNotInstalledExceptionMessage = 'El módulo Microsoft.PowerShell.SecretManagement no está instalado.' + noPathSuppliedForRouteExceptionMessage = 'No se proporcionó una ruta para la Ruta.' + validationOfAnyOfSchemaNotSupportedExceptionMessage = "La validación de un esquema que incluye 'anyof' no es compatible." + iisAuthSupportIsForWindowsOnlyExceptionMessage = 'El soporte de autenticación IIS es solo para Windows.' + oauth2InnerSchemeInvalidExceptionMessage = 'OAuth2 InnerScheme solo puede ser Basic o Form, pero se obtuvo: {0}' + noRoutePathSuppliedForPageExceptionMessage = 'No se proporcionó ruta de acceso para la página {0}.' + cacheStorageNotFoundForExistsExceptionMessage = "No se encontró el almacenamiento en caché con el nombre '{0}' al intentar comprobar si el elemento en caché '{1}' existe." + handlerAlreadyDefinedExceptionMessage = '[{0}] {1}: Manejador ya definido.' + sessionsNotConfiguredExceptionMessage = 'Las sesiones no se han configurado.' + propertiesTypeObjectAssociationExceptionMessage = 'Solo las propiedades de tipo Objeto pueden estar asociadas con {0}.' + sessionsRequiredForSessionPersistentAuthExceptionMessage = 'Se requieren sesiones para usar la autenticación persistente de sesión.' + invalidPathWildcardOrDirectoryExceptionMessage = 'La ruta suministrada no puede ser un comodín o un directorio: {0}' + accessMethodAlreadyDefinedExceptionMessage = 'Método de acceso ya definido: {0}' + parametersValueOrExternalValueMandatoryExceptionMessage = "Los parámetros 'Value' o 'ExternalValue' son obligatorios." + maximumConcurrentTasksInvalidExceptionMessage = 'El número máximo de tareas concurrentes debe ser >=1, pero se obtuvo: {0}' + cannotCreatePropertyWithoutTypeExceptionMessage = 'No se puede crear la propiedad porque no se ha definido ningún tipo.' + authMethodNotExistForMergingExceptionMessage = 'El método de autenticación no existe para la fusión: {0}' + maxValueInvalidExceptionMessage = "El valor máximo '{0}' para {1} no es válido, debe ser menor o igual a {2}" + endpointAlreadyDefinedExceptionMessage = "Ya se ha definido un punto de conexión llamado '{0}'." + eventAlreadyRegisteredExceptionMessage = 'Evento {0} ya registrado: {1}' + parameterNotSuppliedInRequestExceptionMessage = "No se ha proporcionado un parámetro llamado '{0}' en la solicitud o no hay datos disponibles." + cacheStorageNotFoundForSetExceptionMessage = "No se encontró el almacenamiento en caché con el nombre '{0}' al intentar establecer el elemento en caché '{1}'." + methodPathAlreadyDefinedExceptionMessage = '[{0}] {1}: Ya está definido.' + errorLoggingAlreadyEnabledExceptionMessage = 'El registro de errores ya está habilitado.' + valueForUsingVariableNotFoundExceptionMessage = "No se pudo encontrar el valor para '`$using:{0}'." + rapidPdfDoesNotSupportOpenApi31ExceptionMessage = 'La herramienta de documentación RapidPdf no admite OpenAPI 3.1' + oauth2ClientSecretRequiredExceptionMessage = 'OAuth2 requiere un Client Secret cuando no se usa PKCE.' + invalidBase64JwtExceptionMessage = 'Valor Base64 no válido encontrado en JWT' + noSessionToCalculateDataHashExceptionMessage = 'No hay ninguna sesión disponible para calcular el hash de datos.' + cacheStorageNotFoundForRemoveExceptionMessage = "No se encontró el almacenamiento en caché con el nombre '{0}' al intentar eliminar el elemento en caché '{1}'." + csrfMiddlewareNotInitializedExceptionMessage = 'El Middleware CSRF no se ha inicializado.' + infoTitleMandatoryMessage = 'info.title es obligatorio.' + typeCanOnlyBeAssociatedWithObjectExceptionMessage = 'El tipo {0} solo se puede asociar con un Objeto.' + userFileDoesNotExistExceptionMessage = 'El archivo de usuario no existe: {0}' + routeParameterNeedsValidScriptblockExceptionMessage = 'El parámetro Route necesita un ScriptBlock válido y no vacío.' + nextTriggerCalculationErrorExceptionMessage = 'Parece que algo salió mal al intentar calcular la siguiente fecha y hora del disparador: {0}' + cannotLockValueTypeExceptionMessage = 'No se puede bloquear un [ValueType].' + failedToCreateOpenSslCertExceptionMessage = 'Error al crear el certificado OpenSSL: {0}' + jwtExpiredExceptionMessage = 'El JWT ha expirado.' + openingGuiMessage = 'Abriendo la GUI.' + multiTypePropertiesRequireOpenApi31ExceptionMessage = 'Las propiedades de tipo múltiple requieren OpenApi versión 3.1 o superior.' + noNameForWebSocketRemoveExceptionMessage = 'No se proporcionó ningún nombre para eliminar el WebSocket.' + maxSizeInvalidExceptionMessage = 'MaxSize debe ser igual o mayor que 0, pero se obtuvo: {0}' + iisShutdownMessage = '(Apagado de IIS)' + cannotUnlockValueTypeExceptionMessage = 'No se puede desbloquear un [ValueType].' + noJwtSignatureForAlgorithmExceptionMessage = 'No se proporcionó una firma JWT para {0}.' + maximumConcurrentWebSocketThreadsInvalidExceptionMessage = 'El número máximo de hilos concurrentes de WebSocket debe ser >=1, pero se obtuvo: {0}' + acknowledgeMessageOnlySupportedOnSmtpTcpEndpointsExceptionMessage = 'El mensaje de reconocimiento solo es compatible con endpoints SMTP y TCP.' + failedToConnectToUrlExceptionMessage = 'Error al conectar con la URL: {0}' + failedToAcquireMutexOwnershipExceptionMessage = 'No se pudo adquirir la propiedad del mutex. Nombre del mutex: {0}' + sessionsRequiredForOAuth2WithPKCEExceptionMessage = 'Se requieren sesiones para usar OAuth2 con PKCE.' + failedToConnectToWebSocketExceptionMessage = 'Error al conectar con el WebSocket: {0}' + unsupportedObjectExceptionMessage = 'Objeto no compatible' + failedToParseAddressExceptionMessage = "Error al analizar '{0}' como una dirección IP/Host:Puerto válida" + mustBeRunningWithAdminPrivilegesExceptionMessage = 'Debe estar ejecutándose con privilegios de administrador para escuchar en direcciones que no sean localhost.' + specificationMessage = 'Especificación' + cacheStorageNotFoundForClearExceptionMessage = "No se encontró el almacenamiento en caché con el nombre '{0}' al intentar vaciar la caché." + restartingServerMessage = 'Reiniciando el servidor...' + cannotSupplyIntervalWhenEveryIsNoneExceptionMessage = "No se puede proporcionar un intervalo cuando el parámetro 'Every' está configurado en None." + unsupportedJwtAlgorithmExceptionMessage = 'El algoritmo JWT actualmente no es compatible: {0}' + websocketsNotConfiguredForSignalMessagesExceptionMessage = 'WebSockets no están configurados para enviar mensajes de señal.' + invalidLogicTypeInHashtableMiddlewareExceptionMessage = 'Un Middleware Hashtable suministrado tiene un tipo de lógica no válido. Se esperaba ScriptBlock, pero se obtuvo: {0}' + maximumConcurrentSchedulesLessThanMinimumExceptionMessage = 'Las programaciones simultáneos máximos no pueden ser inferiores al mínimo de {0} pero se obtuvo: {1}' + failedToAcquireSemaphoreOwnershipExceptionMessage = 'No se pudo adquirir la propiedad del semáforo. Nombre del semáforo: {0}' + propertiesParameterWithoutNameExceptionMessage = 'Los parámetros de propiedades no se pueden usar si la propiedad no tiene nombre.' + customSessionStorageMethodNotImplementedExceptionMessage = "El almacenamiento de sesión personalizado no implementa el método requerido '{0}()'." + authenticationMethodDoesNotExistExceptionMessage = 'El método de autenticación no existe: {0}' + webhooksFeatureNotSupportedInOpenApi30ExceptionMessage = 'La función de Webhooks no es compatible con OpenAPI v3.0.x' + invalidContentTypeForSchemaExceptionMessage = "'content-type' inválido encontrado para el esquema: {0}" + noUnlockScriptBlockForVaultExceptionMessage = "No se suministró ningún ScriptBlock de desbloqueo para desbloquear la bóveda '{0}'" + definitionTagMessage = 'Definición {0}:' + failedToOpenRunspacePoolExceptionMessage = 'Error al abrir RunspacePool: {0}' + failedToCloseRunspacePoolExceptionMessage = 'No se pudo cerrar el RunspacePool: {0}' + verbNoLogicPassedExceptionMessage = '[Verbo] {0}: No se pasó ninguna lógica' + noMutexFoundExceptionMessage = "No se encontró ningún mutex llamado '{0}'" + documentationMessage = 'Documentación' + timerAlreadyDefinedExceptionMessage = '[Temporizador] {0}: Temporizador ya definido.' + invalidPortExceptionMessage = 'El puerto no puede ser negativo: {0}' + viewsFolderNameAlreadyExistsExceptionMessage = 'El nombre de la carpeta Views ya existe: {0}' + noNameForWebSocketResetExceptionMessage = 'No se proporcionó ningún nombre para restablecer el WebSocket.' + mergeDefaultAuthNotInListExceptionMessage = "La autenticación MergeDefault '{0}' no está en la lista de autenticación proporcionada." + descriptionRequiredExceptionMessage = 'Se requiere una descripción para la Ruta:{0} Respuesta:{1}' + pageNameShouldBeAlphaNumericExceptionMessage = 'El nombre de la página debe ser un valor alfanumérico válido: {0}' + defaultValueNotBooleanOrEnumExceptionMessage = 'El valor predeterminado no es un booleano y no forma parte del enum.' + openApiComponentSchemaDoesNotExistExceptionMessage = 'El esquema del componente OpenApi {0} no existe.' + timerParameterMustBeGreaterThanZeroExceptionMessage = '[Temporizador] {0}: {1} debe ser mayor que 0.' + taskTimedOutExceptionMessage = 'La tarea ha agotado el tiempo después de {0}ms.' + scheduleStartTimeAfterEndTimeExceptionMessage = "[Programador] {0}: No puede tener un 'StartTime' después del 'EndTime'" + infoVersionMandatoryMessage = 'info.version es obligatorio.' + cannotUnlockNullObjectExceptionMessage = 'No se puede desbloquear un objeto nulo.' + nonEmptyScriptBlockRequiredForCustomAuthExceptionMessage = 'Se requiere un ScriptBlock no vacío para el esquema de autenticación personalizado.' + nonEmptyScriptBlockRequiredForAuthMethodExceptionMessage = 'Se requiere un ScriptBlock no vacío para el método de autenticación.' + validationOfOneOfSchemaNotSupportedExceptionMessage = "La validación de un esquema que incluye 'oneof' no es compatible." + routeParameterCannotBeNullExceptionMessage = "El parámetro 'Route' no puede ser nulo." + cacheStorageAlreadyExistsExceptionMessage = "Ya existe un almacenamiento en caché con el nombre '{0}'." + loggingMethodRequiresValidScriptBlockExceptionMessage = "El método de salida proporcionado para el método de registro '{0}' requiere un ScriptBlock válido." + scopedVariableAlreadyDefinedExceptionMessage = 'La variable con alcance ya está definida: {0}' + oauth2RequiresAuthorizeUrlExceptionMessage = 'OAuth2 requiere que se proporcione una URL de autorización.' + pathNotExistExceptionMessage = 'La ruta no existe: {0}' + noDomainServerNameForWindowsAdAuthExceptionMessage = 'No se ha proporcionado un nombre de servidor de dominio para la autenticación AD de Windows.' + suppliedDateAfterScheduleEndTimeExceptionMessage = 'La fecha proporcionada es posterior a la hora de finalización del programación en {0}' + wildcardMethodsIncompatibleWithAutoMethodsExceptionMessage = 'El comodín * para los Métodos es incompatible con el interruptor AutoMethods.' + cannotSupplyIntervalForYearExceptionMessage = 'No se puede proporcionar un valor de intervalo para cada año.' + missingComponentsMessage = 'Componente(s) faltante(s)' + invalidStrictTransportSecurityDurationExceptionMessage = 'Duración de Strict-Transport-Security no válida proporcionada: {0}. Debe ser mayor que 0.' + noSecretForHmac512ExceptionMessage = 'No se suministró ningún secreto para el hash HMAC512.' + daysInMonthExceededExceptionMessage = '{0} solo tiene {1} días, pero se suministró {2}.' + nonEmptyScriptBlockRequiredForCustomLoggingExceptionMessage = 'Se requiere un ScriptBlock no vacío para el método de salida de registro personalizado.' + encodingAttributeOnlyAppliesToMultipartExceptionMessage = 'El atributo de codificación solo se aplica a cuerpos de solicitud multipart y application/x-www-form-urlencoded.' + suppliedDateBeforeScheduleStartTimeExceptionMessage = 'La fecha proporcionada es anterior a la hora de inicio del programación en {0}' + unlockSecretRequiredExceptionMessage = "Se requiere una propiedad 'UnlockSecret' al usar Microsoft.PowerShell.SecretStore" + noLogicPassedForMethodRouteExceptionMessage = '[{0}] {1}: No se pasó lógica.' + bodyParserAlreadyDefinedForContentTypeExceptionMessage = 'Un analizador de cuerpo ya está definido para el tipo de contenido {0}.' + invalidJwtSuppliedExceptionMessage = 'JWT proporcionado no válido.' + sessionsRequiredForFlashMessagesExceptionMessage = 'Se requieren sesiones para usar mensajes Flash.' + semaphoreAlreadyExistsExceptionMessage = 'Ya existe un semáforo con el siguiente nombre: {0}' + invalidJwtHeaderAlgorithmSuppliedExceptionMessage = 'Algoritmo del encabezado JWT proporcionado no válido.' + oauth2ProviderDoesNotSupportPasswordGrantTypeExceptionMessage = "El proveedor de OAuth2 no admite el tipo de concesión 'password' requerido al usar un InnerScheme." + invalidAliasFoundExceptionMessage = 'Se encontró un alias {0} no válido: {1}' + scheduleDoesNotExistExceptionMessage = "El programación '{0}' no existe." + accessMethodNotExistExceptionMessage = 'El método de acceso no existe: {0}' + oauth2ProviderDoesNotSupportCodeResponseTypeExceptionMessage = "El proveedor de OAuth2 no admite el tipo de respuesta 'code'." + untestedPowerShellVersionWarningMessage = '[ADVERTENCIA] Pode {0} no se ha probado en PowerShell {1}, ya que no estaba disponible cuando se lanzó Pode.' + secretVaultAlreadyRegisteredAutoImportExceptionMessage = "Ya se ha registrado un Bóveda Secreta con el nombre '{0}' al importar automáticamente Bóvedas Secretas." + schemeRequiresValidScriptBlockExceptionMessage = "El esquema proporcionado para el validador de autenticación '{0}' requiere un ScriptBlock válido." + serverLoopingMessage = 'Bucle del servidor cada {0} segundos' + certificateThumbprintsNameSupportedOnWindowsExceptionMessage = 'Las huellas digitales/nombres de certificados solo son compatibles con Windows.' + sseConnectionNameRequiredExceptionMessage = "Se requiere un nombre de conexión SSE, ya sea de -Name o `$WebEvent.Sse.Name" + invalidMiddlewareTypeExceptionMessage = 'Uno de los Middlewares suministrados es de un tipo no válido. Se esperaba ScriptBlock o Hashtable, pero se obtuvo: {0}' + noSecretForJwtSignatureExceptionMessage = 'No se suministró ningún secreto para la firma JWT.' + modulePathDoesNotExistExceptionMessage = 'La ruta del módulo no existe: {0}' + taskAlreadyDefinedExceptionMessage = '[Tarea] {0}: Tarea ya definida.' + verbAlreadyDefinedExceptionMessage = '[Verbo] {0}: Ya está definido' + clientCertificatesOnlySupportedOnHttpsEndpointsExceptionMessage = 'Los certificados de cliente solo son compatibles con endpoints HTTPS.' + endpointNameNotExistExceptionMessage = "No existe un punto de conexión con el nombre '{0}'." + middlewareNoLogicSuppliedExceptionMessage = '[Middleware]: No se suministró lógica en el ScriptBlock.' + scriptBlockRequiredForMergingUsersExceptionMessage = 'Se requiere un ScriptBlock para fusionar múltiples usuarios autenticados en un solo objeto cuando Valid es All.' + secretVaultAlreadyRegisteredExceptionMessage = "Un Cofre de Secretos con el nombre '{0}' ya ha sido registrado{1}." + deprecatedTitleVersionDescriptionWarningMessage = "ADVERTENCIA: Título, Versión y Descripción en 'Enable-PodeOpenApi' están obsoletos. Utilice 'Add-PodeOAInfo' en su lugar." + undefinedOpenApiReferencesMessage = 'Referencias OpenAPI indefinidas:' + doneMessage = 'Hecho' + swaggerEditorDoesNotSupportOpenApi31ExceptionMessage = 'Esta versión de Swagger-Editor no admite OpenAPI 3.1' + durationMustBeZeroOrGreaterExceptionMessage = 'La duración debe ser igual o mayor a 0, pero se obtuvo: {0}s' + viewsPathDoesNotExistExceptionMessage = 'La ruta de las Views no existe: {0}' + discriminatorIncompatibleWithAllOfExceptionMessage = "El parámetro 'Discriminator' es incompatible con 'allOf'." + noNameForWebSocketSendMessageExceptionMessage = 'No se proporcionó ningún nombre para enviar un mensaje al WebSocket.' + hashtableMiddlewareNoLogicExceptionMessage = 'Un Middleware Hashtable suministrado no tiene lógica definida.' + openApiInfoMessage = 'Información OpenAPI:' + invalidSchemeForAuthValidatorExceptionMessage = "El esquema '{0}' proporcionado para el validador de autenticación '{1}' requiere un ScriptBlock válido." + sseFailedToBroadcastExceptionMessage = 'SSE no pudo transmitir debido al nivel de transmisión SSE definido para {0}: {1}.' + adModuleWindowsOnlyExceptionMessage = 'El módulo de Active Directory solo está disponible en Windows.' + requestLoggingAlreadyEnabledExceptionMessage = 'El registro de solicitudes ya está habilitado.' + invalidAccessControlMaxAgeDurationExceptionMessage = 'Duración inválida para Access-Control-Max-Age proporcionada: {0}. Debe ser mayor que 0.' + openApiDefinitionAlreadyExistsExceptionMessage = 'La definición de OpenAPI con el nombre {0} ya existe.' + renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag no se puede usar dentro de un 'ScriptBlock' de Select-PodeOADefinition." + taskProcessDoesNotExistExceptionMessage = "El proceso de la tarea '{0}' no existe." + scheduleProcessDoesNotExistExceptionMessage = "El proceso del programación '{0}' no existe." + definitionTagChangeNotAllowedExceptionMessage = 'La etiqueta de definición para una Route no se puede cambiar.' + getRequestBodyNotAllowedExceptionMessage = 'Las operaciones {0} no pueden tener un cuerpo de solicitud.' + fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "La función '{0}' no acepta una matriz como entrada de canalización." + unsupportedStreamCompressionEncodingExceptionMessage = 'La codificación de compresión de transmisión no es compatible: {0}' +} \ No newline at end of file diff --git a/src/Locales/fr/Pode.psd1 b/src/Locales/fr/Pode.psd1 new file mode 100644 index 000000000..c543ae136 --- /dev/null +++ b/src/Locales/fr/Pode.psd1 @@ -0,0 +1,295 @@ +@{ + schemaValidationRequiresPowerShell610ExceptionMessage = 'La validation du schéma nécessite PowerShell version 6.1.0 ou supérieure.' + customAccessPathOrScriptBlockRequiredExceptionMessage = "Un chemin ou un ScriptBlock est requis pour obtenir les valeurs d'accès personnalisées." + operationIdMustBeUniqueForArrayExceptionMessage = 'OperationID : {0} doit être unique et ne peut pas être appliqué à un tableau.' + endpointNotDefinedForRedirectingExceptionMessage = "Un point de terminaison nommé '{0}' n'a pas été défini pour la redirection." + filesHaveChangedMessage = 'Les fichiers suivants ont été modifiés :' + iisAspnetcoreTokenMissingExceptionMessage = 'Le jeton IIS ASPNETCORE_TOKEN est manquant.' + minValueGreaterThanMaxExceptionMessage = 'La valeur minimale pour {0} ne doit pas être supérieure à la valeur maximale.' + noLogicPassedForRouteExceptionMessage = 'Aucune logique passée pour la Route: {0}' + scriptPathDoesNotExistExceptionMessage = "Le chemin du script n'existe pas : {0}" + mutexAlreadyExistsExceptionMessage = 'Un mutex avec le nom suivant existe déjà: {0}' + listeningOnEndpointsMessage = 'Écoute sur les {0} point(s) de terminaison suivant(s) [{1} thread(s)] :' + unsupportedFunctionInServerlessContextExceptionMessage = "La fonction {0} n'est pas prise en charge dans un contexte sans serveur." + expectedNoJwtSignatureSuppliedExceptionMessage = "Aucune signature JWT n'était attendue." + secretAlreadyMountedExceptionMessage = "Un Secret avec le nom '{0}' a déjà été monté." + failedToAcquireLockExceptionMessage = "Impossible d'acquérir un verrou sur l'objet." + noPathSuppliedForStaticRouteExceptionMessage = '[{0}]: Aucun chemin fourni pour la Route statique.' + invalidHostnameSuppliedExceptionMessage = "Nom d'hôte fourni invalide: {0}" + authMethodAlreadyDefinedExceptionMessage = "Méthode d'authentification déjà définie : {0}" + csrfCookieRequiresSecretExceptionMessage = "Lors de l'utilisation de cookies pour CSRF, un Secret est requis. Vous pouvez soit fournir un Secret, soit définir le Secret global du Cookie - (Set-PodeCookieSecret '' -Global)" + nonEmptyScriptBlockRequiredForPageRouteExceptionMessage = 'Un ScriptBlock non vide est requis pour créer une route de page.' + noPropertiesMutuallyExclusiveExceptionMessage = "Le paramètre 'NoProperties' est mutuellement exclusif avec 'Properties', 'MinProperties' et 'MaxProperties'." + incompatiblePodeDllExceptionMessage = 'Une version incompatible existante de Pode.DLL {0} est chargée. La version {1} est requise. Ouvrez une nouvelle session Powershell/pwsh et réessayez.' + accessMethodDoesNotExistExceptionMessage = "La méthode d'accès n'existe pas : {0}." + scheduleAlreadyDefinedExceptionMessage = '[Horaire] {0}: Horaire déjà défini.' + secondsValueCannotBeZeroOrLessExceptionMessage = 'La valeur en secondes ne peut pas être 0 ou inférieure pour {0}' + pathToLoadNotFoundExceptionMessage = 'Chemin à charger {0} non trouvé : {1}' + failedToImportModuleExceptionMessage = "Échec de l'importation du module : {0}" + endpointNotExistExceptionMessage = "Un point de terminaison avec le protocole '{0}' et l'adresse '{1}' ou l'adresse locale '{2}' n'existe pas." + terminatingMessage = 'Terminaison...' + noCommandsSuppliedToConvertToRoutesExceptionMessage = 'Aucune commande fournie pour convertir en routes.' + invalidTaskTypeExceptionMessage = "Le type de tâche n'est pas valide, attendu [System.Threading.Tasks.Task] ou [hashtable]." + alreadyConnectedToWebSocketExceptionMessage = "Déjà connecté au WebSocket avec le nom '{0}'" + crlfMessageEndCheckOnlySupportedOnTcpEndpointsExceptionMessage = "La vérification de fin de message CRLF n'est prise en charge que sur les points de terminaison TCP." + testPodeOAComponentSchemaNeedToBeEnabledExceptionMessage = "'Test-PodeOAComponentSchema' doit être activé en utilisant 'Enable-PodeOpenApi -EnableSchemaValidation'" + adModuleNotInstalledExceptionMessage = "Le module Active Directory n'est pas installé." + cronExpressionInvalidExceptionMessage = "L'expression Cron doit uniquement comporter 5 parties : {0}" + noSessionToSetOnResponseExceptionMessage = 'Aucune session disponible pour être définie sur la réponse.' + valueOutOfRangeExceptionMessage = "La valeur '{0}' pour {1} n'est pas valide, elle doit être comprise entre {2} et {3}" + loggingMethodAlreadyDefinedExceptionMessage = 'Méthode de journalisation déjà définie: {0}' + noSecretForHmac256ExceptionMessage = 'Aucun secret fourni pour le hachage HMAC256.' + eolPowerShellWarningMessage = "[AVERTISSEMENT] Pode {0} n'a pas été testé sur PowerShell {1}, car il est en fin de vie." + runspacePoolFailedToLoadExceptionMessage = "{0} RunspacePool n'a pas pu être chargé." + noEventRegisteredExceptionMessage = 'Aucun événement {0} enregistré : {1}' + scheduleCannotHaveNegativeLimitExceptionMessage = '[Horaire] {0}: Ne peut pas avoir de limite négative.' + openApiRequestStyleInvalidForParameterExceptionMessage = 'Le style de la requête OpenApi ne peut pas être {0} pour un paramètre {1}.' + openApiDocumentNotCompliantExceptionMessage = "Le document OpenAPI n'est pas conforme." + taskDoesNotExistExceptionMessage = "La tâche '{0}' n'existe pas." + scopedVariableNotFoundExceptionMessage = "Variable d'étendue non trouvée : {0}" + sessionsRequiredForCsrfExceptionMessage = 'Des sessions sont nécessaires pour utiliser CSRF sauf si vous souhaitez utiliser des cookies.' + nonEmptyScriptBlockRequiredForLoggingMethodExceptionMessage = 'Un ScriptBlock non vide est requis pour la méthode de journalisation.' + credentialsPassedWildcardForHeadersLiteralExceptionMessage = 'Lorsque des Identifiants sont passés, le caractère générique * pour les En-têtes sera pris comme une chaîne littérale et non comme un caractère générique.' + podeNotInitializedExceptionMessage = "Pode n'a pas été initialisé." + multipleEndpointsForGuiMessage = "Plusieurs points de terminaison définis, seul le premier sera utilisé pour l'interface graphique." + operationIdMustBeUniqueExceptionMessage = 'OperationID : {0} doit être unique.' + invalidJsonJwtExceptionMessage = 'Valeur JSON non valide trouvée dans le JWT' + noAlgorithmInJwtHeaderExceptionMessage = "Aucun algorithme fourni dans l'en-tête JWT." + openApiVersionPropertyMandatoryExceptionMessage = 'La propriété Version OpenApi est obligatoire.' + limitValueCannotBeZeroOrLessExceptionMessage = 'La valeur de la limite ne peut pas être 0 ou inférieure pour {0}' + timerDoesNotExistExceptionMessage = "Minuteur '{0}' n'existe pas." + openApiGenerationDocumentErrorMessage = 'Erreur de génération du document OpenAPI :' + routeAlreadyContainsCustomAccessExceptionMessage = "La route '[{0}] {1}' contient déjà un accès personnalisé avec le nom '{2}'" + maximumConcurrentWebSocketThreadsLessThanMinimumExceptionMessage = 'Le nombre maximum de threads WebSocket simultanés ne peut pas être inférieur au minimum de {0}, mais a obtenu : {1}' + middlewareAlreadyDefinedExceptionMessage = '[Middleware] {0}: Middleware déjà défini.' + invalidAtomCharacterExceptionMessage = "Caractère d'atome cron non valide : {0}" + invalidCronAtomFormatExceptionMessage = "Format d'atome cron invalide trouvé: {0}" + cacheStorageNotFoundForRetrieveExceptionMessage = "Le stockage de cache nommé '{0}' est introuvable lors de la tentative de récupération de l'élément mis en cache '{1}'." + headerMustHaveNameInEncodingContextExceptionMessage = "L'en-tête doit avoir un nom lorsqu'il est utilisé dans un contexte de codage." + moduleDoesNotContainFunctionExceptionMessage = 'Le module {0} ne contient pas la fonction {1} à convertir en une Route.' + pathToIconForGuiDoesNotExistExceptionMessage = "Le chemin vers l'icône pour l'interface graphique n'existe pas: {0}" + noTitleSuppliedForPageExceptionMessage = 'Aucun titre fourni pour la page {0}.' + certificateSuppliedForNonHttpsWssEndpointExceptionMessage = 'Certificat fourni pour un point de terminaison non HTTPS/WSS.' + cannotLockNullObjectExceptionMessage = 'Impossible de verrouiller un objet nul.' + showPodeGuiOnlyAvailableOnWindowsExceptionMessage = 'Show-PodeGui est actuellement disponible uniquement pour Windows PowerShell et PowerShell 7+ sur Windows.' + unlockSecretButNoScriptBlockExceptionMessage = 'Secret de déverrouillage fourni pour le type de coffre-fort personnalisé, mais aucun ScriptBlock de déverrouillage fourni.' + invalidIpAddressExceptionMessage = "L'adresse IP fournie n'est pas valide : {0}" + maxDaysInvalidExceptionMessage = 'MaxDays doit être égal ou supérieur à 0, mais a obtenu: {0}' + noRemoveScriptBlockForVaultExceptionMessage = "Aucun ScriptBlock de suppression fourni pour supprimer des secrets du coffre '{0}'" + noSecretExpectedForNoSignatureExceptionMessage = 'Aucun secret attendu pour aucune signature.' + noCertificateFoundExceptionMessage = "Aucun certificat n'a été trouvé dans {0}{1} pour '{2}'" + minValueInvalidExceptionMessage = "La valeur minimale '{0}' pour {1} n'est pas valide, elle doit être supérieure ou égale à {2}" + accessRequiresAuthenticationOnRoutesExceptionMessage = "L'accès nécessite une authentification sur les routes." + noSecretForHmac384ExceptionMessage = 'Aucun secret fourni pour le hachage HMAC384.' + windowsLocalAuthSupportIsForWindowsOnlyExceptionMessage = "Le support de l'authentification locale Windows est uniquement pour Windows." + definitionTagNotDefinedExceptionMessage = 'Tag de définition {0} non défini.' + noComponentInDefinitionExceptionMessage = "Aucun composant du type {0} nommé {1} n'est disponible dans la définition {2}." + noSmtpHandlersDefinedExceptionMessage = 'Aucun gestionnaire SMTP défini.' + sessionMiddlewareAlreadyInitializedExceptionMessage = 'Le Middleware de session a déjà été initialisé.' + reusableComponentPathItemsNotAvailableInOpenApi30ExceptionMessage = "La fonctionnalité du composant réutilisable 'pathItems' n'est pas disponible dans OpenAPI v3.0." + wildcardHeadersIncompatibleWithAutoHeadersExceptionMessage = 'Le caractère générique * pour les En-têtes est incompatible avec le commutateur AutoHeaders.' + noDataForFileUploadedExceptionMessage = "Aucune donnée pour le fichier '{0}' n'a été téléchargée dans la demande." + sseOnlyConfiguredOnEventStreamAcceptHeaderExceptionMessage = "SSE ne peut être configuré que sur les requêtes avec une valeur d'en-tête Accept de text/event-stream." + noSessionAvailableToSaveExceptionMessage = 'Aucune session disponible pour sauvegarder.' + pathParameterRequiresRequiredSwitchExceptionMessage = "Si l'emplacement du paramètre est 'Path', le paramètre switch 'Required' est obligatoire." + noOpenApiUrlSuppliedExceptionMessage = 'Aucune URL OpenAPI fournie pour {0}.' + maximumConcurrentSchedulesInvalidExceptionMessage = 'Les horaires simultanés maximum doivent être >=1 mais obtenu: {0}' + snapinsSupportedOnWindowsPowershellOnlyExceptionMessage = 'Les Snapins sont uniquement pris en charge sur Windows PowerShell.' + eventViewerLoggingSupportedOnWindowsOnlyExceptionMessage = "La journalisation dans le Visualisateur d'événements n'est prise en charge que sous Windows." + parametersMutuallyExclusiveExceptionMessage = "Les paramètres '{0}' et '{1}' sont mutuellement exclusifs." + pathItemsFeatureNotSupportedInOpenApi30ExceptionMessage = "La fonction PathItems n'est pas prise en charge dans OpenAPI v3.0.x" + openApiParameterRequiresNameExceptionMessage = 'Le paramètre OpenApi nécessite un nom spécifié.' + maximumConcurrentTasksLessThanMinimumExceptionMessage = 'Le nombre maximum de tâches simultanées ne peut pas être inférieur au minimum de {0}, mais a obtenu : {1}' + noSemaphoreFoundExceptionMessage = "Aucun sémaphore trouvé appelé '{0}'" + singleValueForIntervalExceptionMessage = "Vous ne pouvez fournir qu'une seule valeur {0} lorsque vous utilisez des intervalles." + jwtNotYetValidExceptionMessage = "Le JWT n'est pas encore valide pour une utilisation." + verbAlreadyDefinedForUrlExceptionMessage = '[Verbe] {0} : Déjà défini pour {1}' + noSecretNamedMountedExceptionMessage = "Aucun Secret nommé '{0}' n'a été monté." + moduleOrVersionNotFoundExceptionMessage = 'Module ou version introuvable sur {0} : {1}@{2}' + noScriptBlockSuppliedExceptionMessage = 'Aucun ScriptBlock fourni.' + noSecretVaultRegisteredExceptionMessage = "Aucun coffre-fort de secrets enregistré sous le nom '{0}'." + nameRequiredForEndpointIfRedirectToSuppliedExceptionMessage = 'Un nom est requis pour le point de terminaison si le paramètre RedirectTo est fourni.' + openApiLicenseObjectRequiresNameExceptionMessage = "L'objet OpenAPI 'license' nécessite la propriété 'name'. Utilisez le paramètre -LicenseName." + sourcePathDoesNotExistForStaticRouteExceptionMessage = "{0}: Le chemin source fourni pour la Route statique n'existe pas: {1}" + noNameForWebSocketDisconnectExceptionMessage = 'Aucun Nom fourni pour déconnecter le WebSocket.' + certificateExpiredExceptionMessage = "Le certificat '{0}' a expiré: {1}" + secretVaultUnlockExpiryDateInPastExceptionMessage = "La date d'expiration du déverrouillage du Coffre-Fort de Secrets est dans le passé (UTC) : {0}" + invalidWebExceptionTypeExceptionMessage = "L'exception est d'un type non valide, doit être soit WebException soit HttpRequestException, mais a obtenu : {0}" + invalidSecretValueTypeExceptionMessage = "La valeur du secret est d'un type non valide. Types attendus : String, SecureString, HashTable, Byte[], ou PSCredential. Mais a obtenu : {0}" + explicitTlsModeOnlySupportedOnSmtpsTcpsEndpointsExceptionMessage = "Le mode TLS explicite n'est pris en charge que sur les points de terminaison SMTPS et TCPS." + discriminatorMappingRequiresDiscriminatorPropertyExceptionMessage = "Le paramètre 'DiscriminatorMapping' ne peut être utilisé que lorsque 'DiscriminatorProperty' est présent." + scriptErrorExceptionMessage = "Erreur '{0}' dans le script {1} {2} (ligne {3}) char {4} en exécutant {5} sur l'objet {6} '{7}' Classe : {8} ClasseBase : {9}" + cannotSupplyIntervalForQuarterExceptionMessage = "Impossible de fournir une valeur d'intervalle pour chaque trimestre." + scheduleEndTimeMustBeInFutureExceptionMessage = '[Horaire] {0}: La valeur de EndTime doit être dans le futur.' + invalidJwtSignatureSuppliedExceptionMessage = 'Signature JWT fournie invalide.' + noSetScriptBlockForVaultExceptionMessage = "Aucun ScriptBlock de configuration fourni pour mettre à jour/créer des secrets dans le coffre '{0}'" + accessMethodNotExistForMergingExceptionMessage = "La méthode d'accès n'existe pas pour la fusion : {0}" + defaultAuthNotInListExceptionMessage = "L'authentification par défaut '{0}' n'est pas dans la liste d'authentification fournie." + parameterHasNoNameExceptionMessage = "Le paramètre n'a pas de nom. Veuillez donner un nom à ce composant en utilisant le paramètre 'Name'." + methodPathAlreadyDefinedForUrlExceptionMessage = '[{0}] {1} : Déjà défini pour {2}' + fileWatcherAlreadyDefinedExceptionMessage = "Un Observateur de fichiers nommé '{0}' a déjà été défini." + noServiceHandlersDefinedExceptionMessage = 'Aucun gestionnaire de service défini.' + secretRequiredForCustomSessionStorageExceptionMessage = "Un secret est requis lors de l'utilisation d'un stockage de session personnalisé." + secretManagementModuleNotInstalledExceptionMessage = "Le module Microsoft.PowerShell.SecretManagement n'est pas installé." + noPathSuppliedForRouteExceptionMessage = 'Aucun chemin fourni pour la route.' + validationOfAnyOfSchemaNotSupportedExceptionMessage = "La validation d'un schéma qui inclut 'anyof' n'est pas prise en charge." + iisAuthSupportIsForWindowsOnlyExceptionMessage = "Le support de l'authentification IIS est uniquement pour Windows." + oauth2InnerSchemeInvalidExceptionMessage = 'Le OAuth2 InnerScheme ne peut être que Basic ou Form, mais obtenu : {0}' + noRoutePathSuppliedForPageExceptionMessage = 'Aucun chemin de route fourni pour la page {0}.' + cacheStorageNotFoundForExistsExceptionMessage = "Le stockage de cache nommé '{0}' est introuvable lors de la tentative de vérification de l'existence de l'élément mis en cache '{1}'." + handlerAlreadyDefinedExceptionMessage = '[{0}] {1}: Handler déjà défini.' + sessionsNotConfiguredExceptionMessage = "Les sessions n'ont pas été configurées." + propertiesTypeObjectAssociationExceptionMessage = 'Seules les propriétés de type Objet peuvent être associées à {0}.' + sessionsRequiredForSessionPersistentAuthExceptionMessage = "Des sessions sont nécessaires pour utiliser l'authentification persistante par session." + invalidPathWildcardOrDirectoryExceptionMessage = 'Le chemin fourni ne peut pas être un caractère générique ou un répertoire : {0}' + accessMethodAlreadyDefinedExceptionMessage = "Méthode d'accès déjà définie : {0}" + parametersValueOrExternalValueMandatoryExceptionMessage = "Les paramètres 'Value' ou 'ExternalValue' sont obligatoires." + maximumConcurrentTasksInvalidExceptionMessage = 'Le nombre maximum de tâches simultanées doit être >=1, mais a obtenu : {0}' + cannotCreatePropertyWithoutTypeExceptionMessage = "Impossible de créer la propriété car aucun type n'est défini." + authMethodNotExistForMergingExceptionMessage = "La méthode d'authentification n'existe pas pour la fusion : {0}" + maxValueInvalidExceptionMessage = "La valeur maximale '{0}' pour {1} n'est pas valide, elle doit être inférieure ou égale à {2}" + endpointAlreadyDefinedExceptionMessage = "Un point de terminaison nommé '{0}' a déjà été défini." + eventAlreadyRegisteredExceptionMessage = 'Événement {0} déjà enregistré : {1}' + parameterNotSuppliedInRequestExceptionMessage = "Un paramètre nommé '{0}' n'a pas été fourni dans la demande ou aucune donnée n'est disponible." + cacheStorageNotFoundForSetExceptionMessage = "Le stockage de cache nommé '{0}' est introuvable lors de la tentative de définition de l'élément mis en cache '{1}'." + methodPathAlreadyDefinedExceptionMessage = '[{0}] {1} : Déjà défini.' + errorLoggingAlreadyEnabledExceptionMessage = 'La journalisation des erreurs est déjà activée.' + valueForUsingVariableNotFoundExceptionMessage = "Valeur pour '`$using:{0}' introuvable." + rapidPdfDoesNotSupportOpenApi31ExceptionMessage = "L'outil de documentation RapidPdf ne prend pas en charge OpenAPI 3.1" + oauth2ClientSecretRequiredExceptionMessage = "OAuth2 nécessite un Client Secret lorsque PKCE n'est pas utilisé." + invalidBase64JwtExceptionMessage = 'Valeur encodée en Base64 non valide trouvée dans le JWT' + noSessionToCalculateDataHashExceptionMessage = 'Aucune session disponible pour calculer le hachage de données.' + cacheStorageNotFoundForRemoveExceptionMessage = "Le stockage de cache nommé '{0}' est introuvable lors de la tentative de suppression de l'élément mis en cache '{1}'." + csrfMiddlewareNotInitializedExceptionMessage = "Le Middleware CSRF n'a pas été initialisé." + infoTitleMandatoryMessage = 'info.title est obligatoire.' + typeCanOnlyBeAssociatedWithObjectExceptionMessage = "Le type {0} ne peut être associé qu'à un Objet." + userFileDoesNotExistExceptionMessage = "Le fichier utilisateur n'existe pas : {0}" + routeParameterNeedsValidScriptblockExceptionMessage = 'Le paramètre de la route nécessite un ScriptBlock valide et non vide.' + nextTriggerCalculationErrorExceptionMessage = 'Il semble que quelque chose ait mal tourné lors de la tentative de calcul de la prochaine date et heure de déclenchement : {0}' + cannotLockValueTypeExceptionMessage = 'Impossible de verrouiller un [ValueType].' + failedToCreateOpenSslCertExceptionMessage = 'Échec de la création du certificat OpenSSL : {0}' + jwtExpiredExceptionMessage = 'Le JWT a expiré.' + openingGuiMessage = "Ouverture de l'interface graphique." + multiTypePropertiesRequireOpenApi31ExceptionMessage = 'Les propriétés multi-types nécessitent OpenApi Version 3.1 ou supérieure.' + noNameForWebSocketRemoveExceptionMessage = 'Aucun Nom fourni pour supprimer le WebSocket.' + maxSizeInvalidExceptionMessage = 'MaxSize doit être égal ou supérieur à 0, mais a obtenu: {0}' + iisShutdownMessage = "(Arrêt de l'IIS)" + cannotUnlockValueTypeExceptionMessage = 'Impossible de déverrouiller un [ValueType].' + noJwtSignatureForAlgorithmExceptionMessage = 'Aucune signature JWT fournie pour {0}.' + maximumConcurrentWebSocketThreadsInvalidExceptionMessage = 'Le nombre maximum de threads WebSocket simultanés doit être >=1, mais a obtenu : {0}' + acknowledgeMessageOnlySupportedOnSmtpTcpEndpointsExceptionMessage = "Le message de reconnaissance n'est pris en charge que sur les points de terminaison SMTP et TCP." + failedToConnectToUrlExceptionMessage = "Échec de la connexion à l'URL : {0}" + failedToAcquireMutexOwnershipExceptionMessage = "Échec de l'acquisition de la propriété du mutex. Nom du mutex: {0}" + sessionsRequiredForOAuth2WithPKCEExceptionMessage = 'Des sessions sont nécessaires pour utiliser OAuth2 avec PKCE.' + failedToConnectToWebSocketExceptionMessage = 'Échec de la connexion au WebSocket : {0}' + unsupportedObjectExceptionMessage = 'Objet non pris en charge' + failedToParseAddressExceptionMessage = "Échec de l'analyse de '{0}' en tant qu'adresse IP/Hôte:Port valide" + mustBeRunningWithAdminPrivilegesExceptionMessage = 'Doit être exécuté avec des privilèges administratifs pour écouter sur des adresses autres que localhost.' + specificationMessage = 'Spécification' + cacheStorageNotFoundForClearExceptionMessage = "Le stockage de cache nommé '{0}' est introuvable lors de la tentative de vider le cache." + restartingServerMessage = 'Redémarrage du serveur...' + cannotSupplyIntervalWhenEveryIsNoneExceptionMessage = "Impossible de fournir un intervalle lorsque le paramètre 'Every' est défini sur None." + unsupportedJwtAlgorithmExceptionMessage = "L'algorithme JWT n'est actuellement pas pris en charge : {0}" + websocketsNotConfiguredForSignalMessagesExceptionMessage = 'Les WebSockets ne sont pas configurés pour envoyer des messages de signal.' + invalidLogicTypeInHashtableMiddlewareExceptionMessage = 'Un Middleware Hashtable fourni a un type de logique non valide. Attendu ScriptBlock, mais a obtenu : {0}' + maximumConcurrentSchedulesLessThanMinimumExceptionMessage = 'Les Horaires simultanés maximum ne peuvent pas être inférieurs au minimum de {0} mais obtenu: {1}' + failedToAcquireSemaphoreOwnershipExceptionMessage = "Échec de l'acquisition de la propriété du sémaphore. Nom du sémaphore: {0}" + propertiesParameterWithoutNameExceptionMessage = "Les paramètres Properties ne peuvent pas être utilisés si la propriété n'a pas de nom." + customSessionStorageMethodNotImplementedExceptionMessage = "Le stockage de session personnalisé n'implémente pas la méthode requise '{0}()'." + authenticationMethodDoesNotExistExceptionMessage = "La méthode d'authentification n'existe pas : {0}" + webhooksFeatureNotSupportedInOpenApi30ExceptionMessage = "La fonction Webhooks n'est pas prise en charge dans OpenAPI v3.0.x" + invalidContentTypeForSchemaExceptionMessage = "'content-type' invalide trouvé pour le schéma : {0}" + noUnlockScriptBlockForVaultExceptionMessage = "Aucun ScriptBlock de déverrouillage fourni pour déverrouiller le coffre '{0}'" + definitionTagMessage = 'Définition {0} :' + failedToOpenRunspacePoolExceptionMessage = "Échec de l'ouverture de RunspacePool : {0}" + failedToCloseRunspacePoolExceptionMessage = 'Échec de la fermeture du RunspacePool: {0}' + verbNoLogicPassedExceptionMessage = '[Verbe] {0} : Aucune logique transmise' + noMutexFoundExceptionMessage = "Aucun mutex trouvé appelé '{0}'" + documentationMessage = 'Documentation' + timerAlreadyDefinedExceptionMessage = '[Minuteur] {0}: Minuteur déjà défini.' + invalidPortExceptionMessage = 'Le port ne peut pas être négatif : {0}' + viewsFolderNameAlreadyExistsExceptionMessage = 'Le nom du dossier Views existe déjà: {0}' + noNameForWebSocketResetExceptionMessage = 'Aucun Nom fourni pour réinitialiser le WebSocket.' + mergeDefaultAuthNotInListExceptionMessage = "L'authentification MergeDefault '{0}' n'est pas dans la liste d'authentification fournie." + descriptionRequiredExceptionMessage = 'Une description est requise pour le chemin:{0} Réponse:{1}' + pageNameShouldBeAlphaNumericExceptionMessage = 'Le nom de la page doit être une valeur alphanumérique valide: {0}' + defaultValueNotBooleanOrEnumExceptionMessage = "La valeur par défaut n'est pas un booléen et ne fait pas partie de l'énumération." + openApiComponentSchemaDoesNotExistExceptionMessage = "Le schéma du composant OpenApi {0} n'existe pas." + timerParameterMustBeGreaterThanZeroExceptionMessage = '[Minuteur] {0}: {1} doit être supérieur à 0.' + taskTimedOutExceptionMessage = 'La tâche a expiré après {0}ms.' + scheduleStartTimeAfterEndTimeExceptionMessage = "[Horaire] {0}: Ne peut pas avoir un 'StartTime' après 'EndTime'" + infoVersionMandatoryMessage = 'info.version est obligatoire.' + cannotUnlockNullObjectExceptionMessage = 'Impossible de déverrouiller un objet nul.' + nonEmptyScriptBlockRequiredForCustomAuthExceptionMessage = "Un ScriptBlock non vide est requis pour le schéma d'authentification personnalisé." + nonEmptyScriptBlockRequiredForAuthMethodExceptionMessage = "Un ScriptBlock non vide est requis pour la méthode d'authentification." + validationOfOneOfSchemaNotSupportedExceptionMessage = "La validation d'un schéma qui inclut 'oneof' n'est pas prise en charge." + routeParameterCannotBeNullExceptionMessage = "Le paramètre 'Route' ne peut pas être nul." + cacheStorageAlreadyExistsExceptionMessage = "Un stockage de cache nommé '{0}' existe déjà." + loggingMethodRequiresValidScriptBlockExceptionMessage = "La méthode de sortie fournie pour la méthode de journalisation '{0}' nécessite un ScriptBlock valide." + scopedVariableAlreadyDefinedExceptionMessage = 'La variable à portée est déjà définie : {0}' + oauth2RequiresAuthorizeUrlExceptionMessage = "OAuth2 nécessite une URL d'autorisation." + pathNotExistExceptionMessage = "Le chemin n'existe pas : {0}" + noDomainServerNameForWindowsAdAuthExceptionMessage = "Aucun nom de serveur de domaine n'a été fourni pour l'authentification Windows AD." + suppliedDateAfterScheduleEndTimeExceptionMessage = "La date fournie est postérieure à l'heure de fin du Horaire à {0}" + wildcardMethodsIncompatibleWithAutoMethodsExceptionMessage = 'Le caractère générique * pour les Méthodes est incompatible avec le commutateur AutoMethods.' + cannotSupplyIntervalForYearExceptionMessage = "Impossible de fournir une valeur d'intervalle pour chaque année." + missingComponentsMessage = 'Composant(s) manquant(s)' + invalidStrictTransportSecurityDurationExceptionMessage = 'Durée Strict-Transport-Security invalide fournie : {0}. Doit être supérieure à 0.' + noSecretForHmac512ExceptionMessage = 'Aucun secret fourni pour le hachage HMAC512.' + daysInMonthExceededExceptionMessage = "{0} n'a que {1} jours, mais {2} a été fourni." + nonEmptyScriptBlockRequiredForCustomLoggingExceptionMessage = 'Un ScriptBlock non vide est requis pour la méthode de journalisation personnalisée.' + encodingAttributeOnlyAppliesToMultipartExceptionMessage = "L'attribut d'encodage s'applique uniquement aux corps de requête multipart et application/x-www-form-urlencoded." + suppliedDateBeforeScheduleStartTimeExceptionMessage = "La date fournie est antérieure à l'heure de début du Horaire à {0}" + unlockSecretRequiredExceptionMessage = "Une propriété 'UnlockSecret' est requise lors de l'utilisation de Microsoft.PowerShell.SecretStore" + noLogicPassedForMethodRouteExceptionMessage = '[{0}] {1}: Aucune logique passée.' + bodyParserAlreadyDefinedForContentTypeExceptionMessage = 'Un analyseur de corps est déjà défini pour le type de contenu {0}.' + invalidJwtSuppliedExceptionMessage = 'JWT fourni invalide.' + sessionsRequiredForFlashMessagesExceptionMessage = 'Des sessions sont nécessaires pour utiliser les messages Flash.' + semaphoreAlreadyExistsExceptionMessage = 'Un sémaphore avec le nom suivant existe déjà: {0}' + invalidJwtHeaderAlgorithmSuppliedExceptionMessage = "Algorithme de l'en-tête JWT fourni invalide." + oauth2ProviderDoesNotSupportPasswordGrantTypeExceptionMessage = "Le fournisseur OAuth2 ne supporte pas le type de subvention 'password' requis par l'utilisation d'un InnerScheme." + invalidAliasFoundExceptionMessage = 'Alias {0} non valide trouvé : {1}' + scheduleDoesNotExistExceptionMessage = "Le Horaire '{0}' n'existe pas." + accessMethodNotExistExceptionMessage = "La méthode d'accès n'existe pas : {0}" + oauth2ProviderDoesNotSupportCodeResponseTypeExceptionMessage = "Le fournisseur OAuth2 ne supporte pas le type de réponse 'code'." + untestedPowerShellVersionWarningMessage = "[AVERTISSEMENT] Pode {0} n'a pas été testé sur PowerShell {1}, car il n'était pas disponible lors de la sortie de Pode." + secretVaultAlreadyRegisteredAutoImportExceptionMessage = "Un coffre-fort secret avec le nom '{0}' a déjà été enregistré lors de l'importation automatique des coffres-forts secrets." + schemeRequiresValidScriptBlockExceptionMessage = "Le schéma fourni pour le validateur d'authentification '{0}' nécessite un ScriptBlock valide." + serverLoopingMessage = 'Boucle du serveur toutes les {0} secondes' + certificateThumbprintsNameSupportedOnWindowsExceptionMessage = 'Les empreintes digitales/Noms de certificat ne sont pris en charge que sous Windows.' + sseConnectionNameRequiredExceptionMessage = "Un nom de connexion SSE est requis, soit de -Name soit de `$WebEvent.Sse.Name" + invalidMiddlewareTypeExceptionMessage = "Un des Middlewares fournis est d'un type non valide. Attendu ScriptBlock ou Hashtable, mais a obtenu : {0}" + noSecretForJwtSignatureExceptionMessage = 'Aucun secret fourni pour la signature JWT.' + modulePathDoesNotExistExceptionMessage = "Le chemin du module n'existe pas : {0}" + taskAlreadyDefinedExceptionMessage = '[Tâche] {0} : Tâche déjà définie.' + verbAlreadyDefinedExceptionMessage = '[Verbe] {0} : Déjà défini' + clientCertificatesOnlySupportedOnHttpsEndpointsExceptionMessage = 'Les certificats client ne sont pris en charge que sur les points de terminaison HTTPS.' + endpointNameNotExistExceptionMessage = "Un point de terminaison avec le nom '{0}' n'existe pas." + middlewareNoLogicSuppliedExceptionMessage = '[Middleware] : Aucune logique fournie dans le ScriptBlock.' + scriptBlockRequiredForMergingUsersExceptionMessage = 'Un ScriptBlock est requis pour fusionner plusieurs utilisateurs authentifiés en un seul objet lorsque Valid est All.' + secretVaultAlreadyRegisteredExceptionMessage = "Un Coffre-Fort de Secrets avec le nom '{0}' a déjà été enregistré{1}." + deprecatedTitleVersionDescriptionWarningMessage = "AVERTISSEMENT : Titre, Version et Description sur 'Enable-PodeOpenApi' sont obsolètes. Veuillez utiliser 'Add-PodeOAInfo' à la place." + undefinedOpenApiReferencesMessage = 'Références OpenAPI non définies :' + doneMessage = 'Terminé' + swaggerEditorDoesNotSupportOpenApi31ExceptionMessage = 'Cette version de Swagger-Editor ne prend pas en charge OpenAPI 3.1' + durationMustBeZeroOrGreaterExceptionMessage = 'La durée doit être égale ou supérieure à 0, mais a obtenu : {0}s' + viewsPathDoesNotExistExceptionMessage = "Le chemin des Views n'existe pas: {0}" + discriminatorIncompatibleWithAllOfExceptionMessage = "Le paramètre 'Discriminator' est incompatible avec 'allOf'." + noNameForWebSocketSendMessageExceptionMessage = 'Aucun Nom fourni pour envoyer un message au WebSocket.' + hashtableMiddlewareNoLogicExceptionMessage = "Un Middleware Hashtable fourni n'a aucune logique définie." + openApiInfoMessage = 'Informations OpenAPI :' + invalidSchemeForAuthValidatorExceptionMessage = "Le schéma '{0}' fourni pour le validateur d'authentification '{1}' nécessite un ScriptBlock valide." + sseFailedToBroadcastExceptionMessage = 'SSE a échoué à diffuser en raison du niveau de diffusion SSE défini pour {0} : {1}.' + adModuleWindowsOnlyExceptionMessage = 'Le module Active Directory est uniquement disponible sur Windows.' + requestLoggingAlreadyEnabledExceptionMessage = 'La journalisation des requêtes est déjà activée.' + invalidAccessControlMaxAgeDurationExceptionMessage = 'Durée Access-Control-Max-Age invalide fournie : {0}. Doit être supérieure à 0.' + openApiDefinitionAlreadyExistsExceptionMessage = 'La définition OpenAPI nommée {0} existe déjà.' + renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag ne peut pas être utilisé à l'intérieur d'un 'ScriptBlock' de Select-PodeOADefinition." + taskProcessDoesNotExistExceptionMessage = "Le processus de la tâche '{0}' n'existe pas." + scheduleProcessDoesNotExistExceptionMessage = "Le processus de l'horaire '{0}' n'existe pas." + definitionTagChangeNotAllowedExceptionMessage = 'Le tag de définition pour une Route ne peut pas être modifié.' + getRequestBodyNotAllowedExceptionMessage = 'Les opérations {0} ne peuvent pas avoir de corps de requête.' + fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "La fonction '{0}' n'accepte pas un tableau en tant qu'entrée de pipeline." + unsupportedStreamCompressionEncodingExceptionMessage = "La compression de flux {0} n'est pas prise en charge." +} + diff --git a/src/Locales/it/Pode.psd1 b/src/Locales/it/Pode.psd1 new file mode 100644 index 000000000..cbc7ebb2c --- /dev/null +++ b/src/Locales/it/Pode.psd1 @@ -0,0 +1,294 @@ +@{ + schemaValidationRequiresPowerShell610ExceptionMessage = 'La convalida dello schema richiede PowerShell versione 6.1.0 o superiore.' + customAccessPathOrScriptBlockRequiredExceptionMessage = 'È necessario un percorso o un ScriptBlock per ottenere i valori di accesso personalizzati.' + operationIdMustBeUniqueForArrayExceptionMessage = 'OperationID: {0} deve essere univoco e non può essere applicato a una matrice.' + endpointNotDefinedForRedirectingExceptionMessage = "Non è stato definito un endpoint denominato '{0}' per il reindirizzamento." + filesHaveChangedMessage = 'I seguenti file sono stati modificati:' + iisAspnetcoreTokenMissingExceptionMessage = 'IIS ASPNETCORE_TOKEN è mancante.' + minValueGreaterThanMaxExceptionMessage = 'Il valore minimo per {0} non deve essere maggiore del valore massimo.' + noLogicPassedForRouteExceptionMessage = "Nessuna logica passata per la 'route': {0}" + scriptPathDoesNotExistExceptionMessage = 'Il percorso dello script non esiste: {0}' + mutexAlreadyExistsExceptionMessage = 'Un mutex con il seguente nome esiste già: {0}' + listeningOnEndpointsMessage = 'In ascolto sui seguenti {0} endpoint [{1} thread]:' + unsupportedFunctionInServerlessContextExceptionMessage = "La funzione {0} non è supportata in un contesto 'serverless'." + expectedNoJwtSignatureSuppliedExceptionMessage = 'La firma JWT è inaspettata.' + secretAlreadyMountedExceptionMessage = "Un 'Secret' con il nome '{0}' è già stato montato." + failedToAcquireLockExceptionMessage = "Impossibile acquisire un blocco sull'oggetto." + noPathSuppliedForStaticRouteExceptionMessage = "[{0}]: Nessun percorso fornito per la 'route' statica." + invalidHostnameSuppliedExceptionMessage = 'Nome host fornito non valido: {0}' + authMethodAlreadyDefinedExceptionMessage = 'Metodo di autenticazione già definito: {0}' + csrfCookieRequiresSecretExceptionMessage = "Quando si usano i cookie per CSRF, è necessario un 'Secret'. Puoi fornire uno o impostare il 'Secret' a livello globale - (Set-PodeCookieSecret '' -Global)" + nonEmptyScriptBlockRequiredForPageRouteExceptionMessage = "È richiesto uno 'ScriptBlock' non vuoto per creare una 'route'." + noPropertiesMutuallyExclusiveExceptionMessage = "Il parametro 'NoProperties' è mutuamente esclusivo con 'Properties', 'MinProperties' e 'MaxProperties'." + incompatiblePodeDllExceptionMessage = "È caricata una versione incompatibile esistente di 'Pode.DLL' {0}. È richiesta la versione {1}. Apri una nuova sessione Powershell/pwsh e riprova." + accessMethodDoesNotExistExceptionMessage = 'Il metodo di accesso non esiste: {0}.' + scheduleAlreadyDefinedExceptionMessage = '[Schedulatore] {0}: Pianificazione già definita.' + secondsValueCannotBeZeroOrLessExceptionMessage = 'Il valore dei secondi non può essere 0 o inferiore per {0}' + pathToLoadNotFoundExceptionMessage = 'Percorso per caricare {0} non trovato: {1}' + failedToImportModuleExceptionMessage = 'Importazione del modulo non riuscita: {0}' + endpointNotExistExceptionMessage = "'Endpoint' con protocollo '{0}' e indirizzo '{1}' o indirizzo locale '{2}' non esiste." + terminatingMessage = 'Terminazione...' + noCommandsSuppliedToConvertToRoutesExceptionMessage = "Nessun comando fornito per convertirlo in 'route'." + invalidTaskTypeExceptionMessage = 'Il tipo di attività non è valido, previsto [System.Threading.Tasks.Task] o [hashtable].' + alreadyConnectedToWebSocketExceptionMessage = "Già connesso al WebSocket con il nome '{0}'" + crlfMessageEndCheckOnlySupportedOnTcpEndpointsExceptionMessage = 'Il controllo di fine messaggio CRLF è supportato solo sugli endpoint TCP.' + testPodeOAComponentSchemaNeedToBeEnabledExceptionMessage = "'Test-PodeOAComponentSchema' deve essere abilitato utilizzando 'Enable-PodeOpenApi -EnableSchemaValidation'" + adModuleNotInstalledExceptionMessage = 'Il modulo Active Directory non è installato.' + cronExpressionInvalidExceptionMessage = "L'espressione Cron dovrebbe essere composta solo da 5 parti: {0}" + noSessionToSetOnResponseExceptionMessage = "Non c'è nessuna sessione disponibile per la risposta." + valueOutOfRangeExceptionMessage = "Il valore '{0}' per {1} non è valido, dovrebbe essere compreso tra {2} e {3}" + loggingMethodAlreadyDefinedExceptionMessage = 'Metodo di registrazione già definito: {0}' + noSecretForHmac256ExceptionMessage = "Nessun segreto fornito per l'hash HMAC256." + eolPowerShellWarningMessage = '[ATTENZIONE] Pode {0} non è stato testato su PowerShell {1}, perche è EOL.' + runspacePoolFailedToLoadExceptionMessage = 'Impossibile caricare RunspacePool per {0}.' + noEventRegisteredExceptionMessage = 'Nessun evento {0} registrato: {1}' + scheduleCannotHaveNegativeLimitExceptionMessage = '[Schedulatore] {0}: Non può avere un limite negativo.' + openApiRequestStyleInvalidForParameterExceptionMessage = 'Lo stile della richiesta OpenAPI non può essere {0} per un parametro {1}.' + openApiDocumentNotCompliantExceptionMessage = 'Il documento non è conforme con le specificazioni OpenAPI.' + taskDoesNotExistExceptionMessage = "L'attività '{0}' non esiste." + scopedVariableNotFoundExceptionMessage = 'Variabile di ambito non trovata: {0}' + sessionsRequiredForCsrfExceptionMessage = 'Le sessioni sono necessarie per utilizzare CSRF a meno che non si vogliano usare i cookie.' + nonEmptyScriptBlockRequiredForLoggingMethodExceptionMessage = 'È richiesto uno ScriptBlock non vuoto per il metodo di registrazione.' + credentialsPassedWildcardForHeadersLiteralExceptionMessage = 'Quando vengono passate le Credenziali, il carattere jolly * per le Intestazioni sarà considerato come una stringa letterale e non come un carattere jolly.' + podeNotInitializedExceptionMessage = 'Pode non è stato inizializzato.' + multipleEndpointsForGuiMessage = 'Sono stati definiti più endpoint, solo il primo sarà utilizzato per la GUI.' + operationIdMustBeUniqueExceptionMessage = 'OperationID: {0} deve essere univoco.' + invalidJsonJwtExceptionMessage = 'Valore JSON non valido trovato in JWT' + noAlgorithmInJwtHeaderExceptionMessage = "Nessun algoritmo fornito nell'header JWT." + openApiVersionPropertyMandatoryExceptionMessage = 'La proprietà versione OpenAPI è obbligatoria.' + limitValueCannotBeZeroOrLessExceptionMessage = 'Il valore limite non può essere 0 o inferiore per {0}' + timerDoesNotExistExceptionMessage = "Timer '{0}' non esiste." + openApiGenerationDocumentErrorMessage = 'Errore nella generazione del documento OpenAPI:' + routeAlreadyContainsCustomAccessExceptionMessage = "Il percorso '[{0}] {1}' contiene già un accesso personalizzato con nome '{2}'" + maximumConcurrentWebSocketThreadsLessThanMinimumExceptionMessage = 'Il numero massimo di thread WebSocket simultanei non può essere inferiore al minimo di {0}, ma è stato ottenuto: {1}' + middlewareAlreadyDefinedExceptionMessage = '[Middleware] {0}: Middleware già definito.' + invalidAtomCharacterExceptionMessage = "Carattere cron 'atom' non valido: {0}" + invalidCronAtomFormatExceptionMessage = "Formato cron 'atom' non valido trovato: {0}" + cacheStorageNotFoundForRetrieveExceptionMessage = "Memoria cache con nome '{0}' non trovata durante il tentativo di recuperare l'elemento memorizzato nella cache '{1}'." + headerMustHaveNameInEncodingContextExceptionMessage = "L'intestazione deve avere un nome quando viene utilizzata in un contesto di codifica." + moduleDoesNotContainFunctionExceptionMessage = "Il modulo {0} non contiene la funzione {1} da convertire in una 'route'." + pathToIconForGuiDoesNotExistExceptionMessage = "Il percorso dell'icona per la GUI non esiste: {0}" + noTitleSuppliedForPageExceptionMessage = 'Nessun titolo fornito per la pagina {0}.' + certificateSuppliedForNonHttpsWssEndpointExceptionMessage = 'Certificato fornito per un endpoint non HTTPS/WSS.' + cannotLockNullObjectExceptionMessage = 'Non è possibile bloccare un oggetto nullo.' + showPodeGuiOnlyAvailableOnWindowsExceptionMessage = 'Show-PodeGui è attualmente disponibile solo per Windows PowerShell e PowerShell 7+ su Windows OS.' + unlockSecretButNoScriptBlockExceptionMessage = "'Secret' di sblocco fornito per tipo di 'Secret Vault' personalizzata, ma nessun ScriptBlock di sblocco è fornito." + invalidIpAddressExceptionMessage = "L'indirizzo IP fornito non è valido: {0}" + maxDaysInvalidExceptionMessage = 'MaxDays deve essere 0 o superiore, ma è stato ottenuto: {0}' + noRemoveScriptBlockForVaultExceptionMessage = "Nessun ScriptBlock fornito per rimuovere 'Secret Vault' '{0}'" + noSecretExpectedForNoSignatureExceptionMessage = "Non era previsto alcun 'Secret' per nessuna firma." + noCertificateFoundExceptionMessage = "Nessun certificato trovato in {0}{1} per '{2}'" + minValueInvalidExceptionMessage = "Il valore minimo '{0}' per {1} non è valido, dovrebbe essere maggiore o uguale a {2}" + accessRequiresAuthenticationOnRoutesExceptionMessage = "L'accesso richiede l'autenticazione sulle rotte." + noSecretForHmac384ExceptionMessage = "Nessun 'Secret' fornito per l'hash HMAC384." + windowsLocalAuthSupportIsForWindowsOnlyExceptionMessage = "Il supporto per l'autenticazione locale di Windows è solo per Windows OS." + definitionTagNotDefinedExceptionMessage = 'Tag di definizione {0} non existe.' + noComponentInDefinitionExceptionMessage = 'Nessun componente del tipo {0} chiamato {1} è disponibile nella definizione {2}.' + noSmtpHandlersDefinedExceptionMessage = 'Non sono stati definiti gestori SMTP.' + sessionMiddlewareAlreadyInitializedExceptionMessage = 'Il Middleware della sessione è già stato inizializzato.' + reusableComponentPathItemsNotAvailableInOpenApi30ExceptionMessage = "La funzione del componente riutilizzabile 'pathItems' non è disponibile in OpenAPI v3.0." + wildcardHeadersIncompatibleWithAutoHeadersExceptionMessage = "Il carattere jolly * per le Intestazioni è incompatibile con l'opzione AutoHeaders." + noDataForFileUploadedExceptionMessage = "Nessun dato per il file '{0}' è stato caricato nella richiesta." + sseOnlyConfiguredOnEventStreamAcceptHeaderExceptionMessage = 'SSE può essere configurato solo su richieste con un valore di intestazione Accept di text/event-stream.' + noSessionAvailableToSaveExceptionMessage = 'Nessuna sessione disponibile per il salvataggio.' + pathParameterRequiresRequiredSwitchExceptionMessage = "Se la posizione del parametro è 'Path', il parametro switch 'Required' è obbligatorio." + noOpenApiUrlSuppliedExceptionMessage = 'Nessun URL OpenAPI fornito per {0}.' + maximumConcurrentSchedulesInvalidExceptionMessage = 'Il numero massimo di schedulazioni concorrenti deve essere >=1 ma invece è: {0}' + snapinsSupportedOnWindowsPowershellOnlyExceptionMessage = 'Gli Snapin sono supportati solo con Windows PowerShell.' + eventViewerLoggingSupportedOnWindowsOnlyExceptionMessage = 'La registrazione nel Visualizzatore eventi è supportata solo su Windows OS.' + parametersMutuallyExclusiveExceptionMessage = "I parametri '{0}' e '{1}' sono mutuamente esclusivi." + pathItemsFeatureNotSupportedInOpenApi30ExceptionMessage = "La funzionalità 'PathItems' non è supportata in OpenAPI v3.0.x" + openApiParameterRequiresNameExceptionMessage = 'Il parametro OpenAPI richiede che un nome sia specificato.' + maximumConcurrentTasksLessThanMinimumExceptionMessage = 'Il numero massimo di attività simultanee non può essere inferiore al minimo di {0}, ma è stato fornito: {1}' + noSemaphoreFoundExceptionMessage = "Nessun semaforo trovato chiamato '{0}'" + singleValueForIntervalExceptionMessage = 'Puoi fornire solo un singolo valore {0} quando si utilizzano gli intervalli.' + jwtNotYetValidExceptionMessage = "JWT non è ancora valido per l'uso." + verbAlreadyDefinedForUrlExceptionMessage = '[Verbo] {0}: Già definito per {1}' + noSecretNamedMountedExceptionMessage = "Nessun 'Secret' con il nome '{0}' è stato montato." + moduleOrVersionNotFoundExceptionMessage = 'Modulo o versione non trovati su {0}: {1}@{2}' + noScriptBlockSuppliedExceptionMessage = "Nessun 'ScriptBlock' fornito." + noSecretVaultRegisteredExceptionMessage = "Nessuna 'Secret Vault' con il nome '{0}' è stato registrata." + nameRequiredForEndpointIfRedirectToSuppliedExceptionMessage = "È richiesto un nome per l'endpoint se viene fornito il parametro 'RedirectTo'." + openApiLicenseObjectRequiresNameExceptionMessage = "L'oggetto OpenAPI 'license' richiede la proprietà 'name'. Utilizzare il parametro -LicenseName." + sourcePathDoesNotExistForStaticRouteExceptionMessage = "{0}: Il percorso sorgente fornito per la 'route' statica non esiste: {1}" + noNameForWebSocketDisconnectExceptionMessage = 'Nessun nome fornito per disconnettere il WebSocket.' + certificateExpiredExceptionMessage = "Il certificato '{0}' è scaduto: {1}" + secretVaultUnlockExpiryDateInPastExceptionMessage = "La data di scadenza per sbloccare la 'Secret Vault' è nel passato (UTC): {0}" + invalidWebExceptionTypeExceptionMessage = "L'eccezione è di un tipo non valido, dovrebbe essere WebException o HttpRequestException, ma invece è: {0}" + invalidSecretValueTypeExceptionMessage = "Il valore 'Secret' è di un tipo non valido. Tipi previsti: String, SecureString, HashTable, Byte[] o PSCredential. Ma ottenuto: {0}" + explicitTlsModeOnlySupportedOnSmtpsTcpsEndpointsExceptionMessage = 'La modalità TLS esplicita è supportata solo sugli endpoint SMTPS e TCPS.' + discriminatorMappingRequiresDiscriminatorPropertyExceptionMessage = "Il parametro 'DiscriminatorMapping' può essere utilizzato solo quando è presente 'DiscriminatorProperty'." + scriptErrorExceptionMessage = "Errore '{0}' nello script {1} {2} (riga {3}) carattere {4} eseguendo {5} su {6} oggetto '{7}' Classe: {8} Classe di base: {9}" + cannotSupplyIntervalForQuarterExceptionMessage = 'Impossibile fornire un valore di intervallo per ogni trimestre.' + scheduleEndTimeMustBeInFutureExceptionMessage = '[Schedulatore] {0}: Il valore di EndTime deve essere nel futuro.' + invalidJwtSignatureSuppliedExceptionMessage = 'Firma JWT fornita non valida.' + noSetScriptBlockForVaultExceptionMessage = "Nessun 'ScriptBlock' fornito per aggiornare/creare 'Secret Vault' '{0}'" + accessMethodNotExistForMergingExceptionMessage = "Il metodo di accesso non esiste per l'unione: {0}" + defaultAuthNotInListExceptionMessage = "L'autenticazione predefinita '{0}' non è nella lista di autenticazione fornita." + parameterHasNoNameExceptionMessage = "Il parametro non ha un nome. Assegna un nome a questo componente usando il parametro 'Name'." + methodPathAlreadyDefinedForUrlExceptionMessage = '[{0}] {1}: Già definito per {2}' + fileWatcherAlreadyDefinedExceptionMessage = "Un 'FileWatcher' con il nome '{0}' è già stato definito." + noServiceHandlersDefinedExceptionMessage = 'Non sono stati definiti gestori di servizio.' + secretRequiredForCustomSessionStorageExceptionMessage = "Un 'Secret' è riquesto quando si utilizza l'archiviazione delle sessioni personalizzata." + secretManagementModuleNotInstalledExceptionMessage = 'Il modulo Microsoft.PowerShell.SecretManagement non è installato.' + noPathSuppliedForRouteExceptionMessage = "Nessun percorso fornito per la 'route'." + validationOfAnyOfSchemaNotSupportedExceptionMessage = "La validazione di uno schema che include 'anyof' non è supportata." + iisAuthSupportIsForWindowsOnlyExceptionMessage = "Il supporto per l'autenticazione IIS è solo per Windows OS." + oauth2InnerSchemeInvalidExceptionMessage = 'OAuth2 InnerScheme può essere solo di tipo Basic o Form, ma non di tipo: {0}' + noRoutePathSuppliedForPageExceptionMessage = "Nessun percorso di 'route' fornito per la pagina {0}." + cacheStorageNotFoundForExistsExceptionMessage = "Memoria cache con nome '{0}' non trovata durante il tentativo di verificare se l'elemento memorizzato nella cache '{1}' esiste." + handlerAlreadyDefinedExceptionMessage = '[{0}] {1}: Handler già definito.' + sessionsNotConfiguredExceptionMessage = 'Le sessioni non sono state configurate.' + propertiesTypeObjectAssociationExceptionMessage = "Solo le proprietà di tipo 'Object' possono essere associate a {0}." + sessionsRequiredForSessionPersistentAuthExceptionMessage = "Sono necessarie sessioni per utilizzare l'autenticazione persistente della sessione." + invalidPathWildcardOrDirectoryExceptionMessage = 'Il percorso fornito non può essere un carattere jolly o una directory: {0}' + accessMethodAlreadyDefinedExceptionMessage = 'Metodo di accesso già definito: {0}' + parametersValueOrExternalValueMandatoryExceptionMessage = "I parametri 'Value' o 'ExternalValue' sono obbligatori." + maximumConcurrentTasksInvalidExceptionMessage = 'Il numero massimo di attività simultanee deve essere >=1, {0} non è valido.' + cannotCreatePropertyWithoutTypeExceptionMessage = 'Impossibile creare la proprietà perché manca la definizione di tipo.' + authMethodNotExistForMergingExceptionMessage = 'Il metodo di autenticazione non esiste per la aggregazione: {0}' + maxValueInvalidExceptionMessage = "Il valore massimo '{0}' per {1} non è valido, dovrebbe essere minore o uguale a {2}" + endpointAlreadyDefinedExceptionMessage = "Un endpoint denominato '{0}' è già stato definito." + eventAlreadyRegisteredExceptionMessage = 'Evento {0} già registrato: {1}' + parameterNotSuppliedInRequestExceptionMessage = "Un parametro chiamato '{0}' non è stato fornito nella richiesta o non ci sono dati disponibili." + cacheStorageNotFoundForSetExceptionMessage = "Memoria cache con nome '{0}' non trovata durante il tentativo di impostare l'elemento memorizzato nella cache '{1}'." + methodPathAlreadyDefinedExceptionMessage = '[{0}] {1}: Già definito.' + errorLoggingAlreadyEnabledExceptionMessage = 'La registrazione degli errori è già abilitata.' + valueForUsingVariableNotFoundExceptionMessage = "Impossibile trovare il valore per '`$using:{0}'." + rapidPdfDoesNotSupportOpenApi31ExceptionMessage = 'Lo strumento di documentazione RapidPdf non supporta OpenAPI 3.1' + oauth2ClientSecretRequiredExceptionMessage = 'OAuth2 richiede un Client Secret quando non si utilizza PKCE.' + invalidBase64JwtExceptionMessage = 'Valore codificato Base64 non valido trovato in JWT' + noSessionToCalculateDataHashExceptionMessage = "Nessuna sessione disponibile per calcolare l'hash dei dati." + cacheStorageNotFoundForRemoveExceptionMessage = "Memoria cache con nome '{0}' non trovata durante il tentativo di rimuovere l'elemento memorizzato nella cache '{1}'." + csrfMiddlewareNotInitializedExceptionMessage = 'Il Middleware CSRF non è stato inizializzato.' + infoTitleMandatoryMessage = 'info.title è obbligatorio.' + typeCanOnlyBeAssociatedWithObjectExceptionMessage = 'Il tipo {0} può essere associato solo a un oggetto.' + userFileDoesNotExistExceptionMessage = 'Il file utente non esiste: {0}' + routeParameterNeedsValidScriptblockExceptionMessage = "Il parametro della 'route' richiede uno ScriptBlock valido e non vuoto." + nextTriggerCalculationErrorExceptionMessage = 'Sembra che ci sia stato un errore nel tentativo di calcolare la prossima data e ora del trigger: {0}' + cannotLockValueTypeExceptionMessage = 'Non è possibile bloccare un [ValueType].' + failedToCreateOpenSslCertExceptionMessage = 'Impossibile creare il certificato OpenSSL: {0}' + jwtExpiredExceptionMessage = 'JWT è scaduto.' + openingGuiMessage = 'Apertura della GUI.' + multiTypePropertiesRequireOpenApi31ExceptionMessage = 'Le proprietà multi-tipo richiedono OpenAPI versione 3.1 o superiore.' + noNameForWebSocketRemoveExceptionMessage = 'Nessun nome fornito per rimuovere il WebSocket.' + maxSizeInvalidExceptionMessage = 'MaxSize deve essere 0 o superiore, ma è stato ottenuto: {0}' + iisShutdownMessage = '(Chiusura IIS)' + cannotUnlockValueTypeExceptionMessage = 'Non è possibile sbloccare un [ValueType].' + noJwtSignatureForAlgorithmExceptionMessage = 'Nessuna firma JWT fornita per {0}.' + maximumConcurrentWebSocketThreadsInvalidExceptionMessage = 'Il numero massimo di thread WebSocket simultanei deve essere >=1, ma è stato ottenuto: {0}' + acknowledgeMessageOnlySupportedOnSmtpTcpEndpointsExceptionMessage = 'Il messaggio di conferma è supportato solo sugli endpoint SMTP e TCP.' + failedToConnectToUrlExceptionMessage = "Impossibile connettersi all'URL: {0}" + failedToAcquireMutexOwnershipExceptionMessage = 'Impossibile acquisire la proprietà del mutex. Nome del mutex: {0}' + sessionsRequiredForOAuth2WithPKCEExceptionMessage = 'Sono necessarie sessioni per utilizzare OAuth2 con PKCE' + failedToConnectToWebSocketExceptionMessage = 'Connessione al WebSocket non riuscita: {0}' + unsupportedObjectExceptionMessage = 'Oggetto non supportato' + failedToParseAddressExceptionMessage = "Impossibile analizzare '{0}' come indirizzo IP/Host:Port valido" + mustBeRunningWithAdminPrivilegesExceptionMessage = 'Deve essere eseguito con privilegi di amministratore per usare indirizzi non locali.' + specificationMessage = 'Specifica' + cacheStorageNotFoundForClearExceptionMessage = "Memoria cache con nome '{0}' non trovata durante il tentativo di cancellare la cache." + restartingServerMessage = 'Riavvio del server...' + cannotSupplyIntervalWhenEveryIsNoneExceptionMessage = "Impossibile fornire un intervallo quando il parametro 'Every' è 'None'." + unsupportedJwtAlgorithmExceptionMessage = "L'algoritmo JWT non è attualmente supportato: {0}" + websocketsNotConfiguredForSignalMessagesExceptionMessage = 'I WebSockets non sono configurati per inviare messaggi di segnale.' + invalidLogicTypeInHashtableMiddlewareExceptionMessage = "Un Middleware di tipo Hashtable fornito ha un tipo di logica non valido. Previsto 'ScriptBlock', invece di: {0}" + maximumConcurrentSchedulesLessThanMinimumExceptionMessage = 'Il numero di schedulazioni concorrenti massime non può essere inferiore al minimo di {0}. Valore passato: {1}' + failedToAcquireSemaphoreOwnershipExceptionMessage = 'Impossibile acquisire la proprietà del semaforo. Nome del semaforo: {0}' + propertiesParameterWithoutNameExceptionMessage = "I parametri 'Properties' non possono essere utilizzati se la proprietà non ha un nome." + customSessionStorageMethodNotImplementedExceptionMessage = "L'archiviazione delle sessioni personalizzata non implementa il metodo richiesto '{0}()'." + authenticationMethodDoesNotExistExceptionMessage = 'Il metodo di autenticazione non esiste: {0}' + webhooksFeatureNotSupportedInOpenApi30ExceptionMessage = 'La funzionalità Webhooks non è supportata in OpenAPI v3.0.x' + invalidContentTypeForSchemaExceptionMessage = "'content-type' non valido trovato per lo schema: {0}" + noUnlockScriptBlockForVaultExceptionMessage = "Nessun 'ScriptBlock' di sblocco fornito per sbloccare la 'Secret Vault' '{0}'" + definitionTagMessage = 'Definizione {0}:' + failedToOpenRunspacePoolExceptionMessage = 'Impossibile aprire RunspacePool: {0}' + failedToCloseRunspacePoolExceptionMessage = 'Impossibile chiudere RunspacePool: {0}' + verbNoLogicPassedExceptionMessage = '[Verbo] {0}: Nessuna logica passata' + noMutexFoundExceptionMessage = "Nessun mutex trovato chiamato '{0}'" + documentationMessage = 'Documentazione' + timerAlreadyDefinedExceptionMessage = '[Timer] {0}: Timer già definito.' + invalidPortExceptionMessage = 'La porta non può essere un numero negativo: {0}' + viewsFolderNameAlreadyExistsExceptionMessage = "Il nome della cartella 'Views' esiste già: {0}" + noNameForWebSocketResetExceptionMessage = 'Nessun nome fornito per reimpostare il WebSocket.' + mergeDefaultAuthNotInListExceptionMessage = "L'autenticazione MergeDefault '{0}' non è nella lista di autenticazione fornita." + descriptionRequiredExceptionMessage = 'È necessaria una descrizione per il percorso:{0} Risposta:{1}' + pageNameShouldBeAlphaNumericExceptionMessage = 'Il nome della pagina dovrebbe essere un valore alfanumerico valido: {0}' + defaultValueNotBooleanOrEnumExceptionMessage = "Il valore predefinito non è un booleano e non fa parte dell'enum." + openApiComponentSchemaDoesNotExistExceptionMessage = 'Lo schema del componente OpenAPI {0} non esiste.' + timerParameterMustBeGreaterThanZeroExceptionMessage = '[Timer] {0}: {1} deve essere maggiore di 0.' + taskTimedOutExceptionMessage = "Il 'Task' è scaduto dopo {0}ms." + scheduleStartTimeAfterEndTimeExceptionMessage = "[Schedulatore] {0}: Non può avere un 'StartTime' sucessivo a 'EndTime'" + infoVersionMandatoryMessage = 'info.version è obbligatorio.' + cannotUnlockNullObjectExceptionMessage = 'Non è possibile sbloccare un oggetto nullo.' + nonEmptyScriptBlockRequiredForCustomAuthExceptionMessage = 'È richiesto uno ScriptBlock non vuoto per lo schema di autenticazione personalizzato.' + nonEmptyScriptBlockRequiredForAuthMethodExceptionMessage = 'È necessario un ScriptBlock non vuoto per il metodo di autenticazione.' + validationOfOneOfSchemaNotSupportedExceptionMessage = "La validazione di uno schema che include 'oneof' non è supportata." + routeParameterCannotBeNullExceptionMessage = "Il parametro 'Route' non può essere null." + cacheStorageAlreadyExistsExceptionMessage = "Memoria cache con nome '{0}' esiste già." + loggingMethodRequiresValidScriptBlockExceptionMessage = "Il metodo di output fornito per il metodo di registrazione '{0}' richiede un ScriptBlock valido." + scopedVariableAlreadyDefinedExceptionMessage = 'Variabile con ambito già definita: {0}' + oauth2RequiresAuthorizeUrlExceptionMessage = "OAuth2 richiede che venga fornita un'URL di autorizzazione" + pathNotExistExceptionMessage = 'Il percorso non esiste: {0}' + noDomainServerNameForWindowsAdAuthExceptionMessage = "Non è stato fornito alcun nome di server di dominio per l'autenticazione AD di Windows" + suppliedDateAfterScheduleEndTimeExceptionMessage = "La data fornita è successiva all'ora di fine del programma a {0}" + wildcardMethodsIncompatibleWithAutoMethodsExceptionMessage = "Il carattere jolly * per i Metodi è incompatibile con l'opzione AutoMethods." + cannotSupplyIntervalForYearExceptionMessage = 'Impossibile fornire un valore di intervallo per ogni anno.' + missingComponentsMessage = 'Componenti mancanti' + invalidStrictTransportSecurityDurationExceptionMessage = 'Durata Strict-Transport-Security non valida fornita: {0}. Deve essere maggiore di 0.' + noSecretForHmac512ExceptionMessage = "Nessun 'secret' fornito per l'hash HMAC512." + daysInMonthExceededExceptionMessage = '{0} ha solo {1} giorni, ma è stato fornito {2}.' + nonEmptyScriptBlockRequiredForCustomLoggingExceptionMessage = 'È richiesto uno ScriptBlock non vuoto per il metodo di registrazione personalizzato.' + encodingAttributeOnlyAppliesToMultipartExceptionMessage = "L'attributo di codifica si applica solo ai corpi delle richieste multipart e application/x-www-form-urlencoded." + suppliedDateBeforeScheduleStartTimeExceptionMessage = "La data fornita è precedente all'ora di inizio del programma a {0}" + unlockSecretRequiredExceptionMessage = "È necessaria una proprietà 'UnlockSecret' quando si utilizza Microsoft.PowerShell.SecretStore" + noLogicPassedForMethodRouteExceptionMessage = '[{0}] {1}: Nessuna logica passata.' + bodyParserAlreadyDefinedForContentTypeExceptionMessage = 'Un body-parser è già definito per il tipo di contenuto {0}.' + invalidJwtSuppliedExceptionMessage = 'JWT fornito non valido.' + sessionsRequiredForFlashMessagesExceptionMessage = 'Le sessioni sono necessarie per utilizzare i messaggi di tipo Flash.' + semaphoreAlreadyExistsExceptionMessage = 'Un semaforo con il seguente nome esiste già: {0}' + invalidJwtHeaderAlgorithmSuppliedExceptionMessage = "Algoritmo dell'header JWT fornito non valido." + oauth2ProviderDoesNotSupportPasswordGrantTypeExceptionMessage = "Il provider OAuth2 non supporta il tipo di concessione 'password' richiesto dall'utilizzo di un InnerScheme." + invalidAliasFoundExceptionMessage = 'Alias {0} non valido trovato: {1}' + scheduleDoesNotExistExceptionMessage = "Il programma '{0}' non esiste." + accessMethodNotExistExceptionMessage = 'Il metodo di accesso non esiste: {0}' + oauth2ProviderDoesNotSupportCodeResponseTypeExceptionMessage = "Il provider OAuth2 non supporta il tipo di risposta 'code'." + untestedPowerShellVersionWarningMessage = '[ATTENZIONE] Pode {0} non è stato testato su PowerShell {1}, poiché non era disponibile quando Pode è stato rilasciato.' + secretVaultAlreadyRegisteredAutoImportExceptionMessage = "Una 'Secret Vault' con il nome '{0}' è già stata registrata durante l'importazione automatica delle 'Secret Vaults'." + schemeRequiresValidScriptBlockExceptionMessage = "Lo schema fornito per il validatore di autenticazione '{0}' richiede uno ScriptBlock valido." + serverLoopingMessage = 'Ciclo del server ogni {0} secondi' + certificateThumbprintsNameSupportedOnWindowsExceptionMessage = 'Impronte digitali/nome del certificato supportati solo su Windows OS.' + sseConnectionNameRequiredExceptionMessage = "È richiesto un nome di connessione SSE, sia da -Name che da `$WebEvent.Sse.Name" + invalidMiddlewareTypeExceptionMessage = 'Uno dei Middleware forniti è di un tipo non valido. Previsto ScriptBlock o Hashtable, ma ottenuto: {0}' + noSecretForJwtSignatureExceptionMessage = "Nessun 'secret' fornito per la firma JWT." + modulePathDoesNotExistExceptionMessage = 'Il percorso del modulo non esiste: {0}' + taskAlreadyDefinedExceptionMessage = '[Attività] {0}: Attività già definita.' + verbAlreadyDefinedExceptionMessage = '[Verbo] {0}: Già definito' + clientCertificatesOnlySupportedOnHttpsEndpointsExceptionMessage = 'I certificati client sono supportati solo sugli endpoint HTTPS.' + endpointNameNotExistExceptionMessage = "Endpoint con nome '{0}' non esiste." + middlewareNoLogicSuppliedExceptionMessage = '[Middleware]: Nessuna logica fornita nello ScriptBlock.' + scriptBlockRequiredForMergingUsersExceptionMessage = "È richiesto uno ScriptBlock per unire più utenti autenticati in un unico oggetto quando 'Valid' è uguale a 'All'." + secretVaultAlreadyRegisteredExceptionMessage = "Una 'Secret Vault' con il nome '{0}' è già stato registrata{1}." + deprecatedTitleVersionDescriptionWarningMessage = "ATTENZIONE: Titolo, Versione e Descrizione su 'Enable-PodeOpenApi' sono deprecati. Si prega di utilizzare 'Add-PodeOAInfo' invece." + undefinedOpenApiReferencesMessage = 'Riferimenti OpenAPI non definiti:' + doneMessage = 'Fatto' + swaggerEditorDoesNotSupportOpenApi31ExceptionMessage = 'Questa versione di Swagger-Editor non supporta OpenAPI 3.1' + durationMustBeZeroOrGreaterExceptionMessage = 'La durata deve essere 0 o superiore, non {0}s' + viewsPathDoesNotExistExceptionMessage = 'Il percorso delle Views non esiste: {0}' + discriminatorIncompatibleWithAllOfExceptionMessage = "Il parametro 'Discriminator' è incompatibile con 'allOf'." + noNameForWebSocketSendMessageExceptionMessage = 'Nessun nome fornito per inviare un messaggio al WebSocket.' + hashtableMiddlewareNoLogicExceptionMessage = 'Un Middleware di tipo Hashtable fornito non ha una logica definita.' + openApiInfoMessage = 'Informazioni OpenAPI:' + invalidSchemeForAuthValidatorExceptionMessage = "Lo schema '{0}' fornito per il validatore di autenticazione '{1}' richiede uno ScriptBlock valido." + sseFailedToBroadcastExceptionMessage = 'SSE non è riuscito a trasmettere a causa del livello di trasmissione SSE definito per {0}: {1}.' + adModuleWindowsOnlyExceptionMessage = 'Il modulo Active Directory è disponibile solo su Windows OS.' + requestLoggingAlreadyEnabledExceptionMessage = 'La registrazione delle richieste è già abilitata.' + invalidAccessControlMaxAgeDurationExceptionMessage = 'Durata non valida fornita per Access-Control-Max-Age: {0}. Deve essere maggiore di 0.' + openApiDefinitionAlreadyExistsExceptionMessage = 'La definizione OpenAPI denominata {0} esiste già.' + renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag non può essere utilizzato all'interno di un 'ScriptBlock' di Select-PodeOADefinition." + taskProcessDoesNotExistExceptionMessage = "Il processo dell'attività '{0}' non esiste." + scheduleProcessDoesNotExistExceptionMessage = "Il processo della programma '{0}' non esiste." + definitionTagChangeNotAllowedExceptionMessage = 'Il tag di definizione per una Route non può essere cambiato.' + getRequestBodyNotAllowedExceptionMessage = 'Le operazioni {0} non possono avere un corpo della richiesta.' + fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "La funzione '{0}' non accetta una matrice come input della pipeline." + unsupportedStreamCompressionEncodingExceptionMessage = 'La compressione dello stream non è supportata per la codifica {0}' +} \ No newline at end of file diff --git a/src/Locales/ja/Pode.psd1 b/src/Locales/ja/Pode.psd1 new file mode 100644 index 000000000..5af361d24 --- /dev/null +++ b/src/Locales/ja/Pode.psd1 @@ -0,0 +1,294 @@ +@{ + schemaValidationRequiresPowerShell610ExceptionMessage = 'スキーマ検証には PowerShell バージョン 6.1.0 以上が必要です。' + customAccessPathOrScriptBlockRequiredExceptionMessage = 'カスタムアクセス値のソース化には、パスまたはスクリプトブロックが必要です。' + operationIdMustBeUniqueForArrayExceptionMessage = 'OperationID: {0} は一意でなければならず、配列に適用できません。' + endpointNotDefinedForRedirectingExceptionMessage = "リダイレクトのために名前 '{0}' のエンドポイントが定義されていません。" + filesHaveChangedMessage = '次のファイルが変更されました:' + iisAspnetcoreTokenMissingExceptionMessage = 'IIS ASPNETCORE_TOKENがありません。' + minValueGreaterThanMaxExceptionMessage = '{0}の最小値は最大値を超えることはできません。' + noLogicPassedForRouteExceptionMessage = 'ルートに対してロジックが渡されませんでした: {0}' + scriptPathDoesNotExistExceptionMessage = 'スクリプトパスが存在しません: {0}' + mutexAlreadyExistsExceptionMessage = '次の名前のミューテックスはすでに存在します: {0}' + listeningOnEndpointsMessage = '次の {0} エンドポイントでリッスンしています [{1} スレッド]:' + unsupportedFunctionInServerlessContextExceptionMessage = 'サーバーレスコンテキストではサポートされていない関数です: {0}' + expectedNoJwtSignatureSuppliedExceptionMessage = '提供されるべきではないJWT署名が予期されました。' + secretAlreadyMountedExceptionMessage = "名前 '{0}' のシークレットは既にマウントされています。" + failedToAcquireLockExceptionMessage = 'オブジェクトのロックを取得できませんでした。' + noPathSuppliedForStaticRouteExceptionMessage = '[{0}]: 静的ルートに対して提供されたパスがありません。' + invalidHostnameSuppliedExceptionMessage = '無効なホスト名が指定されました: {0}' + authMethodAlreadyDefinedExceptionMessage = '認証方法はすでに定義されています:{0}' + csrfCookieRequiresSecretExceptionMessage = "CSRFのためにクッキーを使用する場合、秘密が必要です。秘密を提供するか、クッキーのグローバル秘密を設定してください - (Set-PodeCookieSecret '' -Global)" + nonEmptyScriptBlockRequiredForPageRouteExceptionMessage = 'ページルートを作成するには空でないScriptBlockが必要です。' + noPropertiesMutuallyExclusiveExceptionMessage = "パラメーター'NoProperties'は'Properties'、'MinProperties'、および'MaxProperties'と相互排他的です。" + incompatiblePodeDllExceptionMessage = '既存の互換性のないPode.DLLバージョン{0}がロードされています。バージョン{1}が必要です。新しいPowerShell/pwshセッションを開いて再試行してください。' + accessMethodDoesNotExistExceptionMessage = 'アクセスメソッドが存在しません:{0}。' + scheduleAlreadyDefinedExceptionMessage = '[スケジュール] {0}: スケジュールはすでに定義されています。' + secondsValueCannotBeZeroOrLessExceptionMessage = '{0}の秒数値は0またはそれ以下にすることはできません。' + pathToLoadNotFoundExceptionMessage = '読み込むパス{0}が見つかりません: {1}' + failedToImportModuleExceptionMessage = 'モジュールのインポートに失敗しました: {0}' + endpointNotExistExceptionMessage = "プロトコル'{0}'、アドレス'{1}'またはローカルアドレス'{2}'のエンドポイントが存在しません。" + terminatingMessage = '終了中...' + noCommandsSuppliedToConvertToRoutesExceptionMessage = 'ルートに変換するためのコマンドが提供されていません。' + invalidTaskTypeExceptionMessage = 'タスクタイプが無効です。予期されるタイプ:[System.Threading.Tasks.Task]または[hashtable]' + alreadyConnectedToWebSocketExceptionMessage = "名前 '{0}' の WebSocket に既に接続されています" + crlfMessageEndCheckOnlySupportedOnTcpEndpointsExceptionMessage = 'CRLFメッセージ終了チェックはTCPエンドポイントでのみサポートされています。' + testPodeOAComponentSchemaNeedToBeEnabledExceptionMessage = "'Test-PodeOAComponentSchema' は 'Enable-PodeOpenApi -EnableSchemaValidation' を使用して有効にする必要があります。" + adModuleNotInstalledExceptionMessage = 'Active Directoryモジュールがインストールされていません。' + cronExpressionInvalidExceptionMessage = 'Cron式は5つの部分で構成される必要があります: {0}' + noSessionToSetOnResponseExceptionMessage = 'レスポンスに設定するセッションがありません。' + valueOutOfRangeExceptionMessage = "{1}の値'{0}'は無効です。{2}から{3}の間でなければなりません。" + loggingMethodAlreadyDefinedExceptionMessage = 'ログ記録方法は既に定義されています: {0}' + noSecretForHmac256ExceptionMessage = 'HMAC256ハッシュに対する秘密が提供されていません。' + eolPowerShellWarningMessage = '[警告] Pode {0} は、EOLであるPowerShell {1} でテストされていません。' + runspacePoolFailedToLoadExceptionMessage = '{0} RunspacePoolの読み込みに失敗しました。' + noEventRegisteredExceptionMessage = '登録された{0}イベントはありません:{1}' + scheduleCannotHaveNegativeLimitExceptionMessage = '[スケジュール] {0}: 負の制限を持つことはできません。' + openApiRequestStyleInvalidForParameterExceptionMessage = 'OpenApi リクエストのスタイルは {1} パラメータに対して {0} であってはなりません。' + openApiDocumentNotCompliantExceptionMessage = 'OpenAPIドキュメントが準拠していません。' + taskDoesNotExistExceptionMessage = "タスク '{0}' は存在しません。" + scopedVariableNotFoundExceptionMessage = 'スコープ変数が見つかりません: {0}' + sessionsRequiredForCsrfExceptionMessage = 'クッキーを使用しない場合は、CSRFを使用するためにセッションが必要です。' + nonEmptyScriptBlockRequiredForLoggingMethodExceptionMessage = 'ロギングメソッドには空でないScriptBlockが必要です。' + credentialsPassedWildcardForHeadersLiteralExceptionMessage = '資格情報が渡されると、ヘッダーのワイルドカード * はワイルドカードとしてではなく、リテラル文字列として解釈されます。' + podeNotInitializedExceptionMessage = 'Podeが初期化されていません。' + multipleEndpointsForGuiMessage = '複数のエンドポイントが定義されていますが、GUIには最初のエンドポイントのみが使用されます。' + operationIdMustBeUniqueExceptionMessage = 'OperationID: {0} は一意でなければなりません。' + invalidJsonJwtExceptionMessage = 'JWTに無効なJSON値が見つかりました。' + noAlgorithmInJwtHeaderExceptionMessage = 'JWTヘッダーにアルゴリズムが提供されていません。' + openApiVersionPropertyMandatoryExceptionMessage = 'OpenApiバージョンプロパティは必須です。' + limitValueCannotBeZeroOrLessExceptionMessage = '{0}の制限値は0またはそれ以下にすることはできません。' + timerDoesNotExistExceptionMessage = "タイマー '{0}' は存在しません。" + openApiGenerationDocumentErrorMessage = 'OpenAPI生成ドキュメントエラー:' + routeAlreadyContainsCustomAccessExceptionMessage = "ルート '[{0}] {1}' はすでに名前 '{2}' のカスタムアクセスを含んでいます" + maximumConcurrentWebSocketThreadsLessThanMinimumExceptionMessage = '最大同時 WebSocket スレッド数は最小値 {0} より小さくてはいけませんが、取得した値は: {1}' + middlewareAlreadyDefinedExceptionMessage = '[Middleware] {0}: ミドルウェアは既に定義されています。' + invalidAtomCharacterExceptionMessage = '無効なアトム文字: {0}' + invalidCronAtomFormatExceptionMessage = '無効な cron アトム形式が見つかりました: {0}' + cacheStorageNotFoundForRetrieveExceptionMessage = "キャッシュされたアイテム '{1}' を取得しようとしたときに、名前 '{0}' のキャッシュストレージが見つかりません。" + headerMustHaveNameInEncodingContextExceptionMessage = 'エンコーディングコンテキストで使用される場合、ヘッダーには名前が必要です。' + moduleDoesNotContainFunctionExceptionMessage = 'モジュール {0} にはルートに変換する関数 {1} が含まれていません。' + pathToIconForGuiDoesNotExistExceptionMessage = 'GUI用アイコンのパスが存在しません: {0}' + noTitleSuppliedForPageExceptionMessage = '{0} ページのタイトルが提供されていません。' + certificateSuppliedForNonHttpsWssEndpointExceptionMessage = 'HTTPS/WSS以外のエンドポイントに提供された証明書。' + cannotLockNullObjectExceptionMessage = 'nullオブジェクトをロックできません。' + showPodeGuiOnlyAvailableOnWindowsExceptionMessage = 'Show-PodeGuiは現在、Windows PowerShellおよびWindows上のPowerShell 7+でのみ利用可能です。' + unlockSecretButNoScriptBlockExceptionMessage = 'カスタムシークレットボールトタイプに対してアンロックシークレットが提供されましたが、アンロックスクリプトブロックが提供されていません。' + invalidIpAddressExceptionMessage = '提供されたIPアドレスは無効です: {0}' + maxDaysInvalidExceptionMessage = 'MaxDaysは0以上でなければなりませんが、受け取った値は: {0}' + noRemoveScriptBlockForVaultExceptionMessage = "ボールト'{0}'のシークレットを削除するためのスクリプトブロックが提供されていません。" + noSecretExpectedForNoSignatureExceptionMessage = '署名なしのための秘密が提供されることを期待していませんでした。' + noCertificateFoundExceptionMessage = "'{2}'用の{0}{1}に証明書が見つかりませんでした。" + minValueInvalidExceptionMessage = "{1}の最小値'{0}'は無効です。{2}以上でなければなりません。" + accessRequiresAuthenticationOnRoutesExceptionMessage = 'アクセスにはルート上の認証が必要です。' + noSecretForHmac384ExceptionMessage = 'HMAC384ハッシュに対する秘密が提供されていません。' + windowsLocalAuthSupportIsForWindowsOnlyExceptionMessage = 'Windowsローカル認証のサポートはWindowsのみです。' + definitionTagNotDefinedExceptionMessage = '定義タグ {0} が定義されていません。' + noComponentInDefinitionExceptionMessage = '{2}定義に{0}タイプの名前{1}コンポーネントが利用できません。' + noSmtpHandlersDefinedExceptionMessage = 'SMTPハンドラが定義されていません。' + sessionMiddlewareAlreadyInitializedExceptionMessage = 'セッションミドルウェアは既に初期化されています。' + reusableComponentPathItemsNotAvailableInOpenApi30ExceptionMessage = "OpenAPI v3.0では再利用可能なコンポーネント機能'pathItems'は使用できません。" + wildcardHeadersIncompatibleWithAutoHeadersExceptionMessage = 'ヘッダーのワイルドカード * は AutoHeaders スイッチと互換性がありません。' + noDataForFileUploadedExceptionMessage = "リクエストでアップロードされたファイル '{0}' のデータがありません。" + sseOnlyConfiguredOnEventStreamAcceptHeaderExceptionMessage = 'SSEはAcceptヘッダー値がtext/event-streamのリクエストでのみ構成できます。' + noSessionAvailableToSaveExceptionMessage = '保存するためのセッションが利用できません。' + pathParameterRequiresRequiredSwitchExceptionMessage = "パラメータの場所が 'Path' の場合、スイッチパラメータ 'Required' は必須です。" + noOpenApiUrlSuppliedExceptionMessage = '{0} 用の OpenAPI URL が提供されていません。' + maximumConcurrentSchedulesInvalidExceptionMessage = '最大同時スケジュール数は 1 以上でなければなりませんが、受け取った値: {0}' + snapinsSupportedOnWindowsPowershellOnlyExceptionMessage = 'SnapinsはWindows PowerShellのみでサポートされています。' + eventViewerLoggingSupportedOnWindowsOnlyExceptionMessage = 'イベントビューアーロギングはWindowsでのみサポートされています。' + parametersMutuallyExclusiveExceptionMessage = "パラメータ '{0}' と '{1}' は互いに排他的です。" + pathItemsFeatureNotSupportedInOpenApi30ExceptionMessage = 'PathItems機能はOpenAPI v3.0.xではサポートされていません。' + openApiParameterRequiresNameExceptionMessage = 'OpenApi パラメータには名前が必要です。' + maximumConcurrentTasksLessThanMinimumExceptionMessage = '最大同時タスク数は最小値 {0} より少なくてはいけませんが、取得した値は: {1}' + noSemaphoreFoundExceptionMessage = "名前 '{0}' のセマフォが見つかりません" + singleValueForIntervalExceptionMessage = 'インターバルを使用する場合、単一の{0}値しか指定できません。' + jwtNotYetValidExceptionMessage = 'JWTはまだ有効ではありません。' + verbAlreadyDefinedForUrlExceptionMessage = '[動詞] {0}: {1}にすでに定義されています' + noSecretNamedMountedExceptionMessage = "名前 '{0}' のシークレットはマウントされていません。" + moduleOrVersionNotFoundExceptionMessage = '{0}でモジュールまたはバージョンが見つかりません: {1}@{2}' + noScriptBlockSuppliedExceptionMessage = 'ScriptBlockが提供されていません。' + noSecretVaultRegisteredExceptionMessage = "名前 '{0}' のシークレットボールトは登録されていません。" + nameRequiredForEndpointIfRedirectToSuppliedExceptionMessage = 'RedirectToパラメーターが提供されている場合、エンドポイントには名前が必要です。' + openApiLicenseObjectRequiresNameExceptionMessage = "OpenAPI オブジェクト 'license' には 'name' プロパティが必要です。-LicenseName パラメータを使用してください。" + sourcePathDoesNotExistForStaticRouteExceptionMessage = '{0}: 静的ルートに対して提供されたソースパスが存在しません: {1}' + noNameForWebSocketDisconnectExceptionMessage = '切断する WebSocket の名前が指定されていません。' + certificateExpiredExceptionMessage = "証明書 '{0}' の有効期限が切れています: {1}" + secretVaultUnlockExpiryDateInPastExceptionMessage = 'シークレットボールトのアンロック有効期限が過去に設定されています (UTC) :{0}' + invalidWebExceptionTypeExceptionMessage = '例外が無効な型です。WebExceptionまたはHttpRequestExceptionのいずれかである必要がありますが、次の型を取得しました: {0}' + invalidSecretValueTypeExceptionMessage = 'シークレットの値が無効な型です。期待される型: String、SecureString、HashTable、Byte[]、またはPSCredential。しかし、次を取得しました: {0}' + explicitTlsModeOnlySupportedOnSmtpsTcpsEndpointsExceptionMessage = '明示的なTLSモードはSMTPSおよびTCPSエンドポイントでのみサポートされています。' + discriminatorMappingRequiresDiscriminatorPropertyExceptionMessage = "パラメーター'DiscriminatorMapping'は'DiscriminatorProperty'が存在する場合にのみ使用できます。" + scriptErrorExceptionMessage = "スクリプト{1} {2}(行{3})のエラー'{0}'(文字{4})が{6}オブジェクト'{7}'の{5}を実行中に発生しました クラス: {8} 基底クラス: {9}" + cannotSupplyIntervalForQuarterExceptionMessage = '四半期ごとの間隔値を提供できません。' + scheduleEndTimeMustBeInFutureExceptionMessage = '[スケジュール] {0}: EndTime 値は未来に設定する必要があります。' + invalidJwtSignatureSuppliedExceptionMessage = '無効なJWT署名が提供されました。' + noSetScriptBlockForVaultExceptionMessage = "ボールト'{0}'のシークレットを更新/作成するためのスクリプトブロックが提供されていません。" + accessMethodNotExistForMergingExceptionMessage = 'マージするアクセス方法が存在しません: {0}' + defaultAuthNotInListExceptionMessage = "デフォルト認証'{0}'は提供された認証リストにありません。" + parameterHasNoNameExceptionMessage = "パラメーターに名前がありません。このコンポーネントに'Name'パラメーターを使用して名前を付けてください。" + methodPathAlreadyDefinedForUrlExceptionMessage = '[{0}] {1}: {2}用に既に定義されています。' + fileWatcherAlreadyDefinedExceptionMessage = "名前 '{0}' のファイルウォッチャーは既に定義されています。" + noServiceHandlersDefinedExceptionMessage = 'サービスハンドラが定義されていません。' + secretRequiredForCustomSessionStorageExceptionMessage = 'カスタムセッションストレージを使用する場合、シークレットが必要です。' + secretManagementModuleNotInstalledExceptionMessage = 'Microsoft.PowerShell.SecretManagementモジュールがインストールされていません。' + noPathSuppliedForRouteExceptionMessage = 'ルートのパスが提供されていません。' + validationOfAnyOfSchemaNotSupportedExceptionMessage = "'anyof'を含むスキーマの検証はサポートされていません。" + iisAuthSupportIsForWindowsOnlyExceptionMessage = 'IIS認証のサポートはWindowsのみです。' + oauth2InnerSchemeInvalidExceptionMessage = 'OAuth2 InnerSchemeはBasicまたはFormのいずれかでなければなりませんが、取得したのは: {0}' + noRoutePathSuppliedForPageExceptionMessage = '{0} ページのルートパスが提供されていません。' + cacheStorageNotFoundForExistsExceptionMessage = "キャッシュされたアイテム '{1}' が存在するかどうかを確認しようとしたときに、名前 '{0}' のキャッシュストレージが見つかりません。" + handlerAlreadyDefinedExceptionMessage = '[{0}] {1}: ハンドラは既に定義されています。' + sessionsNotConfiguredExceptionMessage = 'セッションが構成されていません。' + propertiesTypeObjectAssociationExceptionMessage = 'Object 型のプロパティのみが {0} と関連付けられます。' + sessionsRequiredForSessionPersistentAuthExceptionMessage = 'セッション持続認証を使用するにはセッションが必要です。' + invalidPathWildcardOrDirectoryExceptionMessage = '指定されたパスはワイルドカードまたはディレクトリにすることはできません: {0}' + accessMethodAlreadyDefinedExceptionMessage = 'アクセス方法はすでに定義されています: {0}' + parametersValueOrExternalValueMandatoryExceptionMessage = "パラメータ 'Value' または 'ExternalValue' は必須です。" + maximumConcurrentTasksInvalidExceptionMessage = '最大同時タスク数は >=1 でなければなりませんが、取得した値は: {0}' + cannotCreatePropertyWithoutTypeExceptionMessage = '型が定義されていないため、プロパティを作成できません。' + authMethodNotExistForMergingExceptionMessage = 'マージするための認証方法は存在しません:{0}' + maxValueInvalidExceptionMessage = "{1}の最大値'{0}'は無効です。{2}以下でなければなりません。" + endpointAlreadyDefinedExceptionMessage = "名前 '{0}' のエンドポイントは既に定義されています。" + eventAlreadyRegisteredExceptionMessage = '{0}イベントはすでに登録されています:{1}' + parameterNotSuppliedInRequestExceptionMessage = "リクエストに '{0}' という名前のパラメータが提供されていないか、データがありません。" + cacheStorageNotFoundForSetExceptionMessage = "キャッシュされたアイテム '{1}' を設定しようとしたときに、名前 '{0}' のキャッシュストレージが見つかりません。" + methodPathAlreadyDefinedExceptionMessage = '[{0}] {1}: 既に定義されています。' + errorLoggingAlreadyEnabledExceptionMessage = 'エラーロギングは既に有効になっています。' + valueForUsingVariableNotFoundExceptionMessage = "'`$using:{0}'の値が見つかりませんでした。" + rapidPdfDoesNotSupportOpenApi31ExceptionMessage = 'ドキュメントツール RapidPdf は OpenAPI 3.1 をサポートしていません' + oauth2ClientSecretRequiredExceptionMessage = 'PKCEを使用しない場合、OAuth2にはクライアントシークレットが必要です。' + invalidBase64JwtExceptionMessage = 'JWTに無効なBase64エンコード値が見つかりました。' + noSessionToCalculateDataHashExceptionMessage = 'データハッシュを計算するセッションがありません。' + cacheStorageNotFoundForRemoveExceptionMessage = "キャッシュされたアイテム '{1}' を削除しようとしたときに、名前 '{0}' のキャッシュストレージが見つかりません。" + csrfMiddlewareNotInitializedExceptionMessage = 'CSRFミドルウェアが初期化されていません。' + infoTitleMandatoryMessage = 'info.title は必須です。' + typeCanOnlyBeAssociatedWithObjectExceptionMessage = 'タイプ{0}はオブジェクトにのみ関連付けることができます。' + userFileDoesNotExistExceptionMessage = 'ユーザーファイルが存在しません:{0}' + routeParameterNeedsValidScriptblockExceptionMessage = 'ルートパラメーターには有効で空でないScriptBlockが必要です。' + nextTriggerCalculationErrorExceptionMessage = '次のトリガー日時の計算中に問題が発生したようです: {0}' + cannotLockValueTypeExceptionMessage = '[ValueType]をロックできません。' + failedToCreateOpenSslCertExceptionMessage = 'OpenSSL証明書の作成に失敗しました: {0}' + jwtExpiredExceptionMessage = 'JWTの有効期限が切れています。' + openingGuiMessage = 'GUIを開いています。' + multiTypePropertiesRequireOpenApi31ExceptionMessage = '複数タイプのプロパティはOpenApiバージョン3.1以上が必要です。' + noNameForWebSocketRemoveExceptionMessage = '削除する WebSocket の名前が指定されていません。' + maxSizeInvalidExceptionMessage = 'MaxSizeは0以上でなければなりませんが、受け取った値は: {0}' + iisShutdownMessage = '(IIS シャットダウン)' + cannotUnlockValueTypeExceptionMessage = '[ValueType]のロックを解除できません。' + noJwtSignatureForAlgorithmExceptionMessage = '{0}のためのJWT署名が提供されていません。' + maximumConcurrentWebSocketThreadsInvalidExceptionMessage = '最大同時 WebSocket スレッド数は >=1 でなければなりませんが、取得した値は: {0}' + acknowledgeMessageOnlySupportedOnSmtpTcpEndpointsExceptionMessage = '確認メッセージはSMTPおよびTCPエンドポイントでのみサポートされています。' + failedToConnectToUrlExceptionMessage = 'URLへの接続に失敗しました: {0}' + failedToAcquireMutexOwnershipExceptionMessage = 'ミューテックスの所有権を取得できませんでした。ミューテックス名: {0}' + sessionsRequiredForOAuth2WithPKCEExceptionMessage = 'PKCEを使用するOAuth2にはセッションが必要です。' + failedToConnectToWebSocketExceptionMessage = 'WebSocket への接続に失敗しました: {0}' + unsupportedObjectExceptionMessage = 'サポートされていないオブジェクトです。' + failedToParseAddressExceptionMessage = "'{0}'を有効なIP/ホスト:ポートアドレスとして解析できませんでした。" + mustBeRunningWithAdminPrivilegesExceptionMessage = 'ローカルホスト以外のアドレスでリッスンするには管理者権限で実行する必要があります。' + specificationMessage = '仕様' + cacheStorageNotFoundForClearExceptionMessage = "キャッシュをクリアしようとしたときに、名前 '{0}' のキャッシュストレージが見つかりません。" + restartingServerMessage = 'サーバーを再起動しています...' + cannotSupplyIntervalWhenEveryIsNoneExceptionMessage = "パラメーター'Every'がNoneに設定されている場合、間隔を提供できません。" + unsupportedJwtAlgorithmExceptionMessage = '現在サポートされていないJWTアルゴリズムです: {0}' + websocketsNotConfiguredForSignalMessagesExceptionMessage = 'WebSocketsはシグナルメッセージを送信するように構成されていません。' + invalidLogicTypeInHashtableMiddlewareExceptionMessage = '提供されたHashtableミドルウェアに無効なロジック型があります。ScriptBlockを期待しましたが、次を取得しました: {0}' + maximumConcurrentSchedulesLessThanMinimumExceptionMessage = '最大同時スケジュール数は最小 {0} 未満にすることはできませんが、受け取った値: {1}' + failedToAcquireSemaphoreOwnershipExceptionMessage = 'セマフォの所有権を取得できませんでした。セマフォ名: {0}' + propertiesParameterWithoutNameExceptionMessage = 'プロパティに名前がない場合、プロパティパラメータは使用できません。' + customSessionStorageMethodNotImplementedExceptionMessage = "カスタムセッションストレージは必要なメソッド'{0}()'を実装していません。" + authenticationMethodDoesNotExistExceptionMessage = '認証方法が存在しません: {0}' + webhooksFeatureNotSupportedInOpenApi30ExceptionMessage = 'Webhooks機能はOpenAPI v3.0.xではサポートされていません。' + invalidContentTypeForSchemaExceptionMessage = "スキーマの 'content-type' が無効です: {0}" + noUnlockScriptBlockForVaultExceptionMessage = "ボールト'{0}'のロック解除に必要なスクリプトブロックが提供されていません。" + definitionTagMessage = '定義 {0}:' + failedToOpenRunspacePoolExceptionMessage = 'RunspacePoolのオープンに失敗しました: {0}' + failedToCloseRunspacePoolExceptionMessage = 'RunspacePoolのクローズに失敗しました: {0}' + verbNoLogicPassedExceptionMessage = '[動詞] {0}: ロジックが渡されていません' + noMutexFoundExceptionMessage = "名前 '{0}' のミューテックスが見つかりません" + documentationMessage = 'ドキュメント' + timerAlreadyDefinedExceptionMessage = '[タイマー] {0}: タイマーはすでに定義されています。' + invalidPortExceptionMessage = 'ポートは負であってはなりません: {0}' + viewsFolderNameAlreadyExistsExceptionMessage = 'ビューのフォルダ名は既に存在します: {0}' + noNameForWebSocketResetExceptionMessage = 'リセットする WebSocket の名前が指定されていません。' + mergeDefaultAuthNotInListExceptionMessage = "MergeDefault認証'{0}'は提供された認証リストにありません。" + descriptionRequiredExceptionMessage = 'パス:{0} 応答:{1} に説明が必要です' + pageNameShouldBeAlphaNumericExceptionMessage = 'ページ名は有効な英数字である必要があります: {0}' + defaultValueNotBooleanOrEnumExceptionMessage = 'デフォルト値は boolean ではなく、enum に含まれていません。' + openApiComponentSchemaDoesNotExistExceptionMessage = 'OpenApi コンポーネントスキーマ {0} は存在しません。' + timerParameterMustBeGreaterThanZeroExceptionMessage = '[タイマー] {0}: {1} は 0 より大きくなければなりません。' + taskTimedOutExceptionMessage = 'タスクが{0}ミリ秒後にタイムアウトしました。' + scheduleStartTimeAfterEndTimeExceptionMessage = "[スケジュール] {0}: 'StartTime' が 'EndTime' の後であることはできません" + infoVersionMandatoryMessage = 'info.version は必須です。' + cannotUnlockNullObjectExceptionMessage = 'nullオブジェクトのロックを解除できません。' + nonEmptyScriptBlockRequiredForCustomAuthExceptionMessage = 'カスタム認証スキームには空でないScriptBlockが必要です。' + nonEmptyScriptBlockRequiredForAuthMethodExceptionMessage = '認証方法には空でない ScriptBlock が必要です。' + validationOfOneOfSchemaNotSupportedExceptionMessage = "'oneof'を含むスキーマの検証はサポートされていません。" + routeParameterCannotBeNullExceptionMessage = "パラメータ 'Route' は null ではいけません。" + cacheStorageAlreadyExistsExceptionMessage = "名前 '{0}' のキャッシュストレージは既に存在します。" + loggingMethodRequiresValidScriptBlockExceptionMessage = "'{0}' ログ記録方法のために提供された出力方法は、有効なScriptBlockが必要です。" + scopedVariableAlreadyDefinedExceptionMessage = 'スコープ付き変数が既に定義されています: {0}' + oauth2RequiresAuthorizeUrlExceptionMessage = 'OAuth2には認可URLの提供が必要です。' + pathNotExistExceptionMessage = 'パスが存在しません: {0}' + noDomainServerNameForWindowsAdAuthExceptionMessage = 'Windows AD認証用のドメインサーバー名が提供されていません。' + suppliedDateAfterScheduleEndTimeExceptionMessage = '提供された日付はスケジュールの終了時間 {0} の後です' + wildcardMethodsIncompatibleWithAutoMethodsExceptionMessage = 'メソッドのワイルドカード * は AutoMethods スイッチと互換性がありません。' + cannotSupplyIntervalForYearExceptionMessage = '毎年の間隔値を提供できません。' + missingComponentsMessage = '欠落しているコンポーネント' + invalidStrictTransportSecurityDurationExceptionMessage = '無効な Strict-Transport-Security 期間が指定されました: {0}。0 より大きい必要があります。' + noSecretForHmac512ExceptionMessage = 'HMAC512ハッシュに対する秘密が提供されていません。' + daysInMonthExceededExceptionMessage = '{0}は{1}日しかありませんが、{2}が指定されました。' + nonEmptyScriptBlockRequiredForCustomLoggingExceptionMessage = 'カスタムロギング出力メソッドには空でないScriptBlockが必要です。' + encodingAttributeOnlyAppliesToMultipartExceptionMessage = 'エンコーディング属性は、multipart および application/x-www-form-urlencoded リクエストボディにのみ適用されます。' + suppliedDateBeforeScheduleStartTimeExceptionMessage = '提供された日付はスケジュールの開始時間 {0} より前です' + unlockSecretRequiredExceptionMessage = "Microsoft.PowerShell.SecretStoreを使用する場合、'UnlockSecret'プロパティが必要です。" + noLogicPassedForMethodRouteExceptionMessage = '[{0}] {1}: ロジックが渡されませんでした。' + bodyParserAlreadyDefinedForContentTypeExceptionMessage = '{0} コンテンツタイプ用のボディパーサーは既に定義されています。' + invalidJwtSuppliedExceptionMessage = '無効なJWTが提供されました。' + sessionsRequiredForFlashMessagesExceptionMessage = 'フラッシュメッセージを使用するにはセッションが必要です。' + semaphoreAlreadyExistsExceptionMessage = '次の名前のセマフォはすでに存在します: {0}' + invalidJwtHeaderAlgorithmSuppliedExceptionMessage = '無効なJWTヘッダーアルゴリズムが提供されました。' + oauth2ProviderDoesNotSupportPasswordGrantTypeExceptionMessage = "OAuth2プロバイダーはInnerSchemeを使用するために必要な'password' grant_typeをサポートしていません。" + invalidAliasFoundExceptionMessage = '無効な{0}エイリアスが見つかりました: {1}' + scheduleDoesNotExistExceptionMessage = "スケジュール '{0}' は存在しません。" + accessMethodNotExistExceptionMessage = 'アクセス方法が存在しません: {0}' + oauth2ProviderDoesNotSupportCodeResponseTypeExceptionMessage = "OAuth2プロバイダーは'code' response_typeをサポートしていません。" + untestedPowerShellVersionWarningMessage = '[警告] Pode {0} はリリース時に利用可能でなかったため、PowerShell {1} でテストされていません。' + secretVaultAlreadyRegisteredAutoImportExceptionMessage = "シークレットボールト'{0}'は既に登録されています(シークレットボールトの自動インポート中)。" + schemeRequiresValidScriptBlockExceptionMessage = "'{0}'認証バリデーターのために提供されたスキームには有効なScriptBlockが必要です。" + serverLoopingMessage = 'サーバーループ間隔 {0}秒' + certificateThumbprintsNameSupportedOnWindowsExceptionMessage = 'Certificate Thumbprints/NameはWindowsでのみサポートされています。' + sseConnectionNameRequiredExceptionMessage = "-Nameまたは`$WebEvent.Sse.NameからSSE接続名が必要です。" + invalidMiddlewareTypeExceptionMessage = '提供されたMiddlewaresの1つが無効な型です。ScriptBlockまたはHashtableのいずれかを期待しましたが、次を取得しました: {0}' + noSecretForJwtSignatureExceptionMessage = 'JWT署名に対する秘密が提供されていません。' + modulePathDoesNotExistExceptionMessage = 'モジュールパスが存在しません: {0}' + taskAlreadyDefinedExceptionMessage = '[タスク] {0}: タスクは既に定義されています。' + verbAlreadyDefinedExceptionMessage = '[動詞] {0}: すでに定義されています' + clientCertificatesOnlySupportedOnHttpsEndpointsExceptionMessage = 'クライアント証明書はHTTPSエンドポイントでのみサポートされています。' + endpointNameNotExistExceptionMessage = "名前'{0}'のエンドポイントが存在しません。" + middlewareNoLogicSuppliedExceptionMessage = '[ミドルウェア]: ScriptBlockにロジックが提供されていません。' + scriptBlockRequiredForMergingUsersExceptionMessage = 'ValidがAllの場合、複数の認証済みユーザーを1つのオブジェクトにマージするためのScriptBlockが必要です。' + secretVaultAlreadyRegisteredExceptionMessage = "名前 '{0}' のシークレットボールトは既に登録されています{1}。" + deprecatedTitleVersionDescriptionWarningMessage = "警告: 'Enable-PodeOpenApi' のタイトル、バージョン、および説明は非推奨です。代わりに 'Add-PodeOAInfo' を使用してください。" + undefinedOpenApiReferencesMessage = '未定義のOpenAPI参照:' + doneMessage = '完了' + swaggerEditorDoesNotSupportOpenApi31ExceptionMessage = 'このバージョンの Swagger-Editor は OpenAPI 3.1 をサポートしていません' + durationMustBeZeroOrGreaterExceptionMessage = '期間は 0 以上でなければなりませんが、取得した値は: {0}s' + viewsPathDoesNotExistExceptionMessage = 'ビューのパスが存在しません: {0}' + discriminatorIncompatibleWithAllOfExceptionMessage = "パラメーター'Discriminator'は'allOf'と互換性がありません。" + noNameForWebSocketSendMessageExceptionMessage = 'メッセージを送信する WebSocket の名前が指定されていません。' + hashtableMiddlewareNoLogicExceptionMessage = '提供されたHashtableミドルウェアにロジックが定義されていません。' + openApiInfoMessage = 'OpenAPI情報:' + invalidSchemeForAuthValidatorExceptionMessage = "'{1}'認証バリデーターのために提供された'{0}'スキームには有効なScriptBlockが必要です。" + sseFailedToBroadcastExceptionMessage = '{0}のSSEブロードキャストレベルが定義されているため、SSEのブロードキャストに失敗しました: {1}' + adModuleWindowsOnlyExceptionMessage = 'Active DirectoryモジュールはWindowsでのみ利用可能です。' + requestLoggingAlreadyEnabledExceptionMessage = 'リクエストロギングは既に有効になっています。' + invalidAccessControlMaxAgeDurationExceptionMessage = '無効な Access-Control-Max-Age 期間が提供されました:{0}。0 より大きくする必要があります。' + openApiDefinitionAlreadyExistsExceptionMessage = '名前が {0} の OpenAPI 定義は既に存在します。' + renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag は Select-PodeOADefinition 'ScriptBlock' 内で使用できません。" + taskProcessDoesNotExistExceptionMessage = 'タスクプロセスが存在しません: {0}' + scheduleProcessDoesNotExistExceptionMessage = 'スケジュールプロセスが存在しません: {0}' + definitionTagChangeNotAllowedExceptionMessage = 'Routeの定義タグは変更できません。' + getRequestBodyNotAllowedExceptionMessage = '{0}操作にはリクエストボディを含めることはできません。' + fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "関数 '{0}' は配列をパイプライン入力として受け付けません。" + unsupportedStreamCompressionEncodingExceptionMessage = 'サポートされていないストリーム圧縮エンコーディングが提供されました: {0}' +} \ No newline at end of file diff --git a/src/Locales/ko/Pode.psd1 b/src/Locales/ko/Pode.psd1 new file mode 100644 index 000000000..26dc8a116 --- /dev/null +++ b/src/Locales/ko/Pode.psd1 @@ -0,0 +1,294 @@ +@{ + schemaValidationRequiresPowerShell610ExceptionMessage = '스키마 유효성 검사는 PowerShell 버전 6.1.0 이상이 필요합니다.' + customAccessPathOrScriptBlockRequiredExceptionMessage = '사용자 지정 액세스 값을 소싱하기 위해 경로 또는 ScriptBlock이 필요합니다.' + operationIdMustBeUniqueForArrayExceptionMessage = 'OperationID: {0}은(는) 고유해야 하며 배열에 적용될 수 없습니다.' + endpointNotDefinedForRedirectingExceptionMessage = "리디렉션을 위해 이름이 '{0}'인 엔드포인트가 정의되지 않았습니다." + filesHaveChangedMessage = '다음 파일이 변경되었습니다:' + iisAspnetcoreTokenMissingExceptionMessage = 'IIS ASPNETCORE_TOKEN이 누락되었습니다.' + minValueGreaterThanMaxExceptionMessage = '{0}의 최소 값은 최대 값보다 클 수 없습니다.' + noLogicPassedForRouteExceptionMessage = '경로에 대한 논리가 전달되지 않았습니다: {0}' + scriptPathDoesNotExistExceptionMessage = '스크립트 경로가 존재하지 않습니다: {0}' + mutexAlreadyExistsExceptionMessage = "이름이 '{0}'인 뮤텍스가 이미 존재합니다." + listeningOnEndpointsMessage = '다음 {0} 엔드포인트에서 수신 중 [{1} 스레드]:' + unsupportedFunctionInServerlessContextExceptionMessage = '{0} 함수는 서버리스 컨텍스트에서 지원되지 않습니다.' + expectedNoJwtSignatureSuppliedExceptionMessage = 'JWT 서명이 제공되지 않을 것으로 예상되었습니다.' + secretAlreadyMountedExceptionMessage = "이름이 '{0}'인 시크릿이 이미 마운트되었습니다." + failedToAcquireLockExceptionMessage = '개체에 대한 잠금을 획득하지 못했습니다.' + noPathSuppliedForStaticRouteExceptionMessage = '[{0}]: 정적 경로에 대한 경로가 제공되지 않았습니다.' + invalidHostnameSuppliedExceptionMessage = '제공된 호스트 이름이 잘못되었습니다: {0}' + authMethodAlreadyDefinedExceptionMessage = '인증 방법이 이미 정의되었습니다: {0}' + csrfCookieRequiresSecretExceptionMessage = "CSRF에 대해 쿠키를 사용할 때, 비밀이 필요합니다. 비밀을 제공하거나 전역 비밀 쿠키를 설정하십시오 - (Set-PodeCookieSecret '' -Global)" + nonEmptyScriptBlockRequiredForPageRouteExceptionMessage = '페이지 경로를 생성하려면 비어 있지 않은 ScriptBlock이 필요합니다.' + noPropertiesMutuallyExclusiveExceptionMessage = "매개변수 'NoProperties'는 'Properties', 'MinProperties' 및 'MaxProperties'와 상호 배타적입니다." + incompatiblePodeDllExceptionMessage = '기존의 호환되지 않는 Pode.DLL 버전 {0}이 로드되었습니다. 버전 {1}이 필요합니다. 새로운 Powershell/pwsh 세션을 열고 다시 시도하세요.' + accessMethodDoesNotExistExceptionMessage = '접근 방법이 존재하지 않습니다: {0}.' + scheduleAlreadyDefinedExceptionMessage = '[스케줄] {0}: 스케줄이 이미 정의되어 있습니다.' + secondsValueCannotBeZeroOrLessExceptionMessage = '{0}에 대한 초 값은 0 이하일 수 없습니다.' + pathToLoadNotFoundExceptionMessage = '로드할 경로 {0}을(를) 찾을 수 없습니다: {1}' + failedToImportModuleExceptionMessage = '모듈을 가져오지 못했습니다: {0}' + endpointNotExistExceptionMessage = "프로토콜 '{0}' 및 주소 '{1}' 또는 로컬 주소 '{2}'가 있는 엔드포인트가 존재하지 않습니다." + terminatingMessage = '종료 중...' + noCommandsSuppliedToConvertToRoutesExceptionMessage = '경로로 변환할 명령이 제공되지 않았습니다.' + invalidTaskTypeExceptionMessage = '작업 유형이 유효하지 않습니다. 예상된 유형: [System.Threading.Tasks.Task] 또는 [hashtable]' + alreadyConnectedToWebSocketExceptionMessage = "이름이 '{0}'인 WebSocket에 이미 연결되어 있습니다." + crlfMessageEndCheckOnlySupportedOnTcpEndpointsExceptionMessage = 'CRLF 메시지 끝 검사는 TCP 엔드포인트에서만 지원됩니다.' + testPodeOAComponentSchemaNeedToBeEnabledExceptionMessage = "'Test-PodeOAComponentSchema'는 'Enable-PodeOpenApi -EnableSchemaValidation'을 사용하여 활성화해야 합니다." + adModuleNotInstalledExceptionMessage = 'Active Directory 모듈이 설치되지 않았습니다.' + cronExpressionInvalidExceptionMessage = 'Cron 표현식은 5개의 부분으로만 구성되어야 합니다: {0}' + noSessionToSetOnResponseExceptionMessage = '응답에 설정할 세션이 없습니다.' + valueOutOfRangeExceptionMessage = "{1}의 값 '{0}'이(가) 유효하지 않습니다. {2}와 {3} 사이여야 합니다." + loggingMethodAlreadyDefinedExceptionMessage = '로깅 방법이 이미 정의되었습니다: {0}' + noSecretForHmac256ExceptionMessage = 'HMAC256 해시를 위한 비밀이 제공되지 않았습니다.' + eolPowerShellWarningMessage = '[경고] Pode {0}은 EOL 상태인 PowerShell {1}에서 테스트되지 않았습니다.' + runspacePoolFailedToLoadExceptionMessage = '{0} RunspacePool 로드 실패.' + noEventRegisteredExceptionMessage = '등록된 {0} 이벤트가 없습니다: {1}' + scheduleCannotHaveNegativeLimitExceptionMessage = '[스케줄] {0}: 음수 한도를 가질 수 없습니다.' + openApiRequestStyleInvalidForParameterExceptionMessage = 'OpenApi 요청 스타일은 {1} 매개변수에 대해 {0}일 수 없습니다.' + openApiDocumentNotCompliantExceptionMessage = 'OpenAPI 문서는 준수하지 않습니다.' + taskDoesNotExistExceptionMessage = "작업 '{0}'이(가) 존재하지 않습니다." + scopedVariableNotFoundExceptionMessage = '범위 변수 {0}을(를) 찾을 수 없습니다.' + sessionsRequiredForCsrfExceptionMessage = '쿠키를 사용하지 않으려면 CSRF 사용을 위해 세션이 필요합니다.' + nonEmptyScriptBlockRequiredForLoggingMethodExceptionMessage = '로깅 방법에는 비어 있지 않은 ScriptBlock이 필요합니다.' + credentialsPassedWildcardForHeadersLiteralExceptionMessage = '자격 증명이 전달되면, 헤더에 대한 * 와일드카드는 와일드카드가 아닌 리터럴 문자열로 취급됩니다.' + podeNotInitializedExceptionMessage = 'Pode가 초기화되지 않았습니다.' + multipleEndpointsForGuiMessage = '여러 엔드포인트가 정의되었으며, GUI에는 첫 번째만 사용됩니다.' + operationIdMustBeUniqueExceptionMessage = 'OperationID: {0}은(는) 고유해야 합니다.' + invalidJsonJwtExceptionMessage = 'JWT에서 잘못된 JSON 값이 발견되었습니다.' + noAlgorithmInJwtHeaderExceptionMessage = 'JWT 헤더에 제공된 알고리즘이 없습니다.' + openApiVersionPropertyMandatoryExceptionMessage = 'OpenApi 버전 속성은 필수입니다.' + limitValueCannotBeZeroOrLessExceptionMessage = '{0}에 대한 제한 값은 0 이하일 수 없습니다.' + timerDoesNotExistExceptionMessage = "타이머 '{0}'이(가) 존재하지 않습니다." + openApiGenerationDocumentErrorMessage = 'OpenAPI 생성 문서 오류:' + routeAlreadyContainsCustomAccessExceptionMessage = "경로 '[{0}] {1}'에 '{2}' 이름의 사용자 지정 액세스가 이미 포함되어 있습니다." + maximumConcurrentWebSocketThreadsLessThanMinimumExceptionMessage = '최대 동시 WebSocket 스레드는 최소값 {0}보다 작을 수 없지만 받은 값: {1}' + middlewareAlreadyDefinedExceptionMessage = '[Middleware] {0}: 미들웨어가 이미 정의되었습니다.' + invalidAtomCharacterExceptionMessage = '잘못된 원자 문자: {0}' + invalidCronAtomFormatExceptionMessage = '잘못된 크론 원자 형식이 발견되었습니다: {0}' + cacheStorageNotFoundForRetrieveExceptionMessage = "캐시된 항목 '{1}'을(를) 검색하려고 할 때 이름이 '{0}'인 캐시 스토리지를 찾을 수 없습니다." + headerMustHaveNameInEncodingContextExceptionMessage = '인코딩 컨텍스트에서 사용될 때 헤더는 이름이 있어야 합니다.' + moduleDoesNotContainFunctionExceptionMessage = '모듈 {0}에 경로로 변환할 함수 {1}이(가) 포함되어 있지 않습니다.' + pathToIconForGuiDoesNotExistExceptionMessage = 'GUI용 아이콘의 경로가 존재하지 않습니다: {0}' + noTitleSuppliedForPageExceptionMessage = '{0} 페이지에 대한 제목이 제공되지 않았습니다.' + certificateSuppliedForNonHttpsWssEndpointExceptionMessage = 'HTTPS/WSS가 아닌 엔드포인트에 제공된 인증서입니다.' + cannotLockNullObjectExceptionMessage = 'null 개체를 잠글 수 없습니다.' + showPodeGuiOnlyAvailableOnWindowsExceptionMessage = 'Show-PodeGui는 현재 Windows PowerShell 및 Windows의 PowerShell 7+에서만 사용할 수 있습니다.' + unlockSecretButNoScriptBlockExceptionMessage = '사용자 정의 비밀 금고 유형에 대해 제공된 Unlock 비밀이지만, Unlock ScriptBlock이 제공되지 않았습니다.' + invalidIpAddressExceptionMessage = '제공된 IP 주소가 유효하지 않습니다: {0}' + maxDaysInvalidExceptionMessage = 'MaxDays는 0 이상이어야 하지만, 받은 값: {0}' + noRemoveScriptBlockForVaultExceptionMessage = "금고 '{0}'에서 비밀을 제거하기 위한 Remove ScriptBlock이 제공되지 않았습니다." + noSecretExpectedForNoSignatureExceptionMessage = '서명이 없는 경우 비밀이 제공되지 않아야 합니다.' + noCertificateFoundExceptionMessage = "'{2}'에 대한 {0}{1}에서 인증서를 찾을 수 없습니다." + minValueInvalidExceptionMessage = "{1}의 최소 값 '{0}'이(가) 유효하지 않습니다. {2} 이상이어야 합니다." + accessRequiresAuthenticationOnRoutesExceptionMessage = '경로에 대한 접근은 인증이 필요합니다.' + noSecretForHmac384ExceptionMessage = 'HMAC384 해시를 위한 비밀이 제공되지 않았습니다.' + windowsLocalAuthSupportIsForWindowsOnlyExceptionMessage = 'Windows 로컬 인증 지원은 Windows 전용입니다.' + definitionTagNotDefinedExceptionMessage = '정의 태그 {0}이(가) 정의되지 않았습니다.' + noComponentInDefinitionExceptionMessage = '{2} 정의에서 {0} 유형의 {1} 이름의 구성 요소가 없습니다.' + noSmtpHandlersDefinedExceptionMessage = '정의된 SMTP 핸들러가 없습니다.' + sessionMiddlewareAlreadyInitializedExceptionMessage = '세션 미들웨어가 이미 초기화되었습니다.' + reusableComponentPathItemsNotAvailableInOpenApi30ExceptionMessage = "OpenAPI v3.0에서는 재사용 가능한 구성 요소 기능 'pathItems'를 사용할 수 없습니다." + wildcardHeadersIncompatibleWithAutoHeadersExceptionMessage = '헤더에 대한 * 와일드카드는 AutoHeaders 스위치와 호환되지 않습니다.' + noDataForFileUploadedExceptionMessage = "요청에서 업로드된 파일 '{0}'에 대한 데이터가 없습니다." + sseOnlyConfiguredOnEventStreamAcceptHeaderExceptionMessage = 'SSE는 Accept 헤더 값이 text/event-stream인 요청에서만 구성할 수 있습니다.' + noSessionAvailableToSaveExceptionMessage = '저장할 수 있는 세션이 없습니다.' + pathParameterRequiresRequiredSwitchExceptionMessage = "매개변수 위치가 'Path'인 경우 'Required' 스위치 매개변수가 필수입니다." + noOpenApiUrlSuppliedExceptionMessage = '{0}에 대한 OpenAPI URL이 제공되지 않았습니다.' + maximumConcurrentSchedulesInvalidExceptionMessage = '최대 동시 스케줄 수는 1 이상이어야 하지만 받은 값: {0}' + snapinsSupportedOnWindowsPowershellOnlyExceptionMessage = 'Snapins는 Windows PowerShell에서만 지원됩니다.' + eventViewerLoggingSupportedOnWindowsOnlyExceptionMessage = '이벤트 뷰어 로깅은 Windows에서만 지원됩니다.' + parametersMutuallyExclusiveExceptionMessage = "매개변수 '{0}'와(과) '{1}'는 상호 배타적입니다." + pathItemsFeatureNotSupportedInOpenApi30ExceptionMessage = 'PathItems 기능은 OpenAPI v3.0.x에서 지원되지 않습니다.' + openApiParameterRequiresNameExceptionMessage = 'OpenApi 매개변수에는 이름이 필요합니다.' + maximumConcurrentTasksLessThanMinimumExceptionMessage = '최대 동시 작업 수는 최소값 {0}보다 작을 수 없지만 받은 값: {1}' + noSemaphoreFoundExceptionMessage = "이름이 '{0}'인 세마포어를 찾을 수 없습니다." + singleValueForIntervalExceptionMessage = '간격을 사용할 때는 단일 {0} 값을 제공할 수 있습니다.' + jwtNotYetValidExceptionMessage = 'JWT가 아직 유효하지 않습니다.' + verbAlreadyDefinedForUrlExceptionMessage = '[동사] {0}: {1}에 대해 이미 정의되었습니다.' + noSecretNamedMountedExceptionMessage = "이름이 '{0}'인 시크릿이 마운트되지 않았습니다." + moduleOrVersionNotFoundExceptionMessage = '{0}에서 모듈 또는 버전을 찾을 수 없습니다: {1}@{2}' + noScriptBlockSuppliedExceptionMessage = 'ScriptBlock이 제공되지 않았습니다.' + noSecretVaultRegisteredExceptionMessage = "이름이 '{0}'인 비밀 금고가 등록되지 않았습니다." + nameRequiredForEndpointIfRedirectToSuppliedExceptionMessage = 'RedirectTo 매개변수가 제공된 경우 엔드포인트에 이름이 필요합니다.' + openApiLicenseObjectRequiresNameExceptionMessage = "OpenAPI 객체 'license'는 'name' 속성이 필요합니다. -LicenseName 매개변수를 사용하십시오." + sourcePathDoesNotExistForStaticRouteExceptionMessage = '{0}: 정적 경로에 대한 제공된 소스 경로가 존재하지 않습니다: {1}' + noNameForWebSocketDisconnectExceptionMessage = '연결을 끊을 WebSocket의 이름이 제공되지 않았습니다.' + certificateExpiredExceptionMessage = "인증서 '{0}'이(가) 만료되었습니다: {1}" + secretVaultUnlockExpiryDateInPastExceptionMessage = '시크릿 금고의 잠금 해제 만료 날짜가 과거입니다 (UTC): {0}' + invalidWebExceptionTypeExceptionMessage = '예외가 잘못된 유형입니다. WebException 또는 HttpRequestException이어야 하지만, 얻은 것은: {0}' + invalidSecretValueTypeExceptionMessage = '비밀 값이 잘못된 유형입니다. 예상되는 유형: String, SecureString, HashTable, Byte[] 또는 PSCredential. 그러나 얻은 것은: {0}' + explicitTlsModeOnlySupportedOnSmtpsTcpsEndpointsExceptionMessage = '명시적 TLS 모드는 SMTPS 및 TCPS 엔드포인트에서만 지원됩니다.' + discriminatorMappingRequiresDiscriminatorPropertyExceptionMessage = "매개변수 'DiscriminatorMapping'은 'DiscriminatorProperty'가 있을 때만 사용할 수 있습니다." + scriptErrorExceptionMessage = "스크립트 {1} {2} (라인 {3}) 문자 {4}에서 {5}을(를) 실행하는 중에 스크립트 {0} 오류가 발생했습니다. 개체 '{7}' 클래스: {8} 기본 클래스: {9}" + cannotSupplyIntervalForQuarterExceptionMessage = '분기별 간격 값을 제공할 수 없습니다.' + scheduleEndTimeMustBeInFutureExceptionMessage = '[스케줄] {0}: 종료 시간 값은 미래에 있어야 합니다.' + invalidJwtSignatureSuppliedExceptionMessage = '제공된 JWT 서명이 유효하지 않습니다.' + noSetScriptBlockForVaultExceptionMessage = "금고 '{0}'에서 비밀을 업데이트/생성하기 위한 Set ScriptBlock이 제공되지 않았습니다." + accessMethodNotExistForMergingExceptionMessage = '병합을 위한 액세스 방법이 존재하지 않습니다: {0}' + defaultAuthNotInListExceptionMessage = "기본 인증 '{0}'이(가) 제공된 인증 목록에 없습니다." + parameterHasNoNameExceptionMessage = "매개변수에 이름이 없습니다. 'Name' 매개변수를 사용하여 이 구성 요소에 이름을 지정하십시오." + methodPathAlreadyDefinedForUrlExceptionMessage = '[{0}] {1}: {2}에 대해 이미 정의되었습니다.' + fileWatcherAlreadyDefinedExceptionMessage = "'{0}'라는 이름의 파일 감시자가 이미 정의되었습니다." + noServiceHandlersDefinedExceptionMessage = '정의된 서비스 핸들러가 없습니다.' + secretRequiredForCustomSessionStorageExceptionMessage = '사용자 정의 세션 저장소를 사용할 때는 비밀이 필요합니다.' + secretManagementModuleNotInstalledExceptionMessage = 'Microsoft.PowerShell.SecretManagement 모듈이 설치되지 않았습니다.' + noPathSuppliedForRouteExceptionMessage = '경로에 대해 제공된 경로가 없습니다.' + validationOfAnyOfSchemaNotSupportedExceptionMessage = "'anyof'을 포함하는 스키마의 유효성 검사는 지원되지 않습니다." + iisAuthSupportIsForWindowsOnlyExceptionMessage = 'IIS 인증 지원은 Windows 전용입니다.' + oauth2InnerSchemeInvalidExceptionMessage = 'OAuth2 InnerScheme은 Basic 또는 Form 인증 중 하나여야 합니다, 그러나 받은 값: {0}' + noRoutePathSuppliedForPageExceptionMessage = '{0} 페이지에 대한 경로가 제공되지 않았습니다.' + cacheStorageNotFoundForExistsExceptionMessage = "캐시된 항목 '{1}'이(가) 존재하는지 확인하려고 할 때 이름이 '{0}'인 캐시 스토리지를 찾을 수 없습니다." + handlerAlreadyDefinedExceptionMessage = '[{0}] {1}: 핸들러가 이미 정의되었습니다.' + sessionsNotConfiguredExceptionMessage = '세션이 구성되지 않았습니다.' + propertiesTypeObjectAssociationExceptionMessage = 'Object 유형의 속성만 {0}와(과) 연결될 수 있습니다.' + sessionsRequiredForSessionPersistentAuthExceptionMessage = '세션 지속 인증을 사용하려면 세션이 필요합니다.' + invalidPathWildcardOrDirectoryExceptionMessage = '제공된 경로는 와일드카드 또는 디렉터리가 될 수 없습니다: {0}' + accessMethodAlreadyDefinedExceptionMessage = '액세스 방법이 이미 정의되었습니다: {0}' + parametersValueOrExternalValueMandatoryExceptionMessage = "매개변수 'Value' 또는 'ExternalValue'는 필수입니다." + maximumConcurrentTasksInvalidExceptionMessage = '최대 동시 작업 수는 >=1이어야 하지만 받은 값: {0}' + cannotCreatePropertyWithoutTypeExceptionMessage = '유형이 정의되지 않았기 때문에 속성을 생성할 수 없습니다.' + authMethodNotExistForMergingExceptionMessage = '병합을 위한 인증 방법이 존재하지 않습니다: {0}' + maxValueInvalidExceptionMessage = "{1}의 최대 값 '{0}'이(가) 유효하지 않습니다. {2} 이하여야 합니다." + endpointAlreadyDefinedExceptionMessage = "이름이 '{0}'인 엔드포인트가 이미 정의되어 있습니다." + eventAlreadyRegisteredExceptionMessage = '{0} 이벤트가 이미 등록되었습니다: {1}' + parameterNotSuppliedInRequestExceptionMessage = "요청에 '{0}'라는 이름의 매개변수가 제공되지 않았거나 데이터가 없습니다." + cacheStorageNotFoundForSetExceptionMessage = "캐시된 항목 '{1}'을(를) 설정하려고 할 때 이름이 '{0}'인 캐시 스토리지를 찾을 수 없습니다." + methodPathAlreadyDefinedExceptionMessage = '[{0}] {1}: 이미 정의되었습니다.' + errorLoggingAlreadyEnabledExceptionMessage = '오류 로깅이 이미 활성화되었습니다.' + valueForUsingVariableNotFoundExceptionMessage = "'`$using:{0}'에 대한 값을 찾을 수 없습니다." + rapidPdfDoesNotSupportOpenApi31ExceptionMessage = '문서 도구 RapidPdf는 OpenAPI 3.1을 지원하지 않습니다.' + oauth2ClientSecretRequiredExceptionMessage = 'PKCE를 사용하지 않을 때 OAuth2에는 클라이언트 비밀이 필요합니다.' + invalidBase64JwtExceptionMessage = 'JWT에서 잘못된 Base64 인코딩 값이 발견되었습니다.' + noSessionToCalculateDataHashExceptionMessage = '데이터 해시를 계산할 세션이 없습니다.' + cacheStorageNotFoundForRemoveExceptionMessage = "캐시된 항목 '{1}'을(를) 제거하려고 할 때 이름이 '{0}'인 캐시 스토리지를 찾을 수 없습니다." + csrfMiddlewareNotInitializedExceptionMessage = 'CSRF 미들웨어가 초기화되지 않았습니다.' + infoTitleMandatoryMessage = 'info.title은 필수 항목입니다.' + typeCanOnlyBeAssociatedWithObjectExceptionMessage = '유형 {0}는 객체와만 연관될 수 있습니다.' + userFileDoesNotExistExceptionMessage = '사용자 파일이 존재하지 않습니다: {0}' + routeParameterNeedsValidScriptblockExceptionMessage = '경로 매개변수에는 유효하고 비어 있지 않은 ScriptBlock이 필요합니다.' + nextTriggerCalculationErrorExceptionMessage = '다음 트리거 날짜 및 시간을 계산하는 중에 문제가 발생한 것 같습니다: {0}' + cannotLockValueTypeExceptionMessage = '[ValueType]를 잠글 수 없습니다.' + failedToCreateOpenSslCertExceptionMessage = 'OpenSSL 인증서 생성 실패: {0}' + jwtExpiredExceptionMessage = 'JWT가 만료되었습니다.' + openingGuiMessage = 'GUI 열기.' + multiTypePropertiesRequireOpenApi31ExceptionMessage = '다중 유형 속성은 OpenApi 버전 3.1 이상이 필요합니다.' + noNameForWebSocketRemoveExceptionMessage = '제거할 WebSocket의 이름이 제공되지 않았습니다.' + maxSizeInvalidExceptionMessage = 'MaxSize는 0 이상이어야 하지만, 받은 값: {0}' + iisShutdownMessage = '(IIS 종료)' + cannotUnlockValueTypeExceptionMessage = '[ValueType]를 잠금 해제할 수 없습니다.' + noJwtSignatureForAlgorithmExceptionMessage = '{0}에 대한 JWT 서명이 제공되지 않았습니다.' + maximumConcurrentWebSocketThreadsInvalidExceptionMessage = '최대 동시 WebSocket 스레드는 >=1이어야 하지만 받은 값: {0}' + acknowledgeMessageOnlySupportedOnSmtpTcpEndpointsExceptionMessage = '확인 메시지는 SMTP 및 TCP 엔드포인트에서만 지원됩니다.' + failedToConnectToUrlExceptionMessage = 'URL에 연결하지 못했습니다: {0}' + failedToAcquireMutexOwnershipExceptionMessage = '뮤텍스 소유권을 획득하지 못했습니다. 뮤텍스 이름: {0}' + sessionsRequiredForOAuth2WithPKCEExceptionMessage = 'PKCE를 사용하는 OAuth2에는 세션이 필요합니다.' + failedToConnectToWebSocketExceptionMessage = 'WebSocket에 연결하지 못했습니다: {0}' + unsupportedObjectExceptionMessage = '지원되지 않는 개체' + failedToParseAddressExceptionMessage = "'{0}'을(를) 유효한 IP/호스트:포트 주소로 구문 분석하지 못했습니다." + mustBeRunningWithAdminPrivilegesExceptionMessage = '관리자 권한으로 실행되어야 비로소 로컬호스트 주소가 아닌 주소를 청취할 수 있습니다.' + specificationMessage = '사양' + cacheStorageNotFoundForClearExceptionMessage = "캐시를 지우려고 할 때 이름이 '{0}'인 캐시 스토리지를 찾을 수 없습니다." + restartingServerMessage = '서버를 재시작 중...' + cannotSupplyIntervalWhenEveryIsNoneExceptionMessage = "매개변수 'Every'가 None으로 설정된 경우 간격을 제공할 수 없습니다." + unsupportedJwtAlgorithmExceptionMessage = 'JWT 알고리즘은 현재 지원되지 않습니다: {0}' + websocketsNotConfiguredForSignalMessagesExceptionMessage = 'WebSockets가 신호 메시지를 보내도록 구성되지 않았습니다.' + invalidLogicTypeInHashtableMiddlewareExceptionMessage = '제공된 Hashtable 미들웨어에 잘못된 논리 유형이 있습니다. 예상된 유형은 ScriptBlock이지만, 얻은 것은: {0}' + maximumConcurrentSchedulesLessThanMinimumExceptionMessage = '최대 동시 스케줄 수는 최소 {0}보다 작을 수 없지만 받은 값: {1}' + failedToAcquireSemaphoreOwnershipExceptionMessage = '세마포어 소유권을 획득하지 못했습니다. 세마포어 이름: {0}' + propertiesParameterWithoutNameExceptionMessage = '속성에 이름이 없으면 Properties 매개변수를 사용할 수 없습니다.' + customSessionStorageMethodNotImplementedExceptionMessage = "사용자 정의 세션 저장소가 필요한 메서드 '{0}()'를 구현하지 않았습니다." + authenticationMethodDoesNotExistExceptionMessage = '인증 방법이 존재하지 않습니다: {0}' + webhooksFeatureNotSupportedInOpenApi30ExceptionMessage = 'Webhooks 기능은 OpenAPI v3.0.x에서 지원되지 않습니다.' + invalidContentTypeForSchemaExceptionMessage = "스키마에 대해 잘못된 'content-type'이 발견되었습니다: {0}" + noUnlockScriptBlockForVaultExceptionMessage = "금고 '{0}'을(를) 해제하는 Unlock ScriptBlock이 제공되지 않았습니다." + definitionTagMessage = '정의 {0}:' + failedToOpenRunspacePoolExceptionMessage = 'RunspacePool을 여는 데 실패했습니다: {0}' + failedToCloseRunspacePoolExceptionMessage = 'RunspacePool을(를) 닫지 못했습니다: {0}' + verbNoLogicPassedExceptionMessage = '[동사] {0}: 전달된 로직 없음' + noMutexFoundExceptionMessage = "이름이 '{0}'인 뮤텍스를 찾을 수 없습니다." + documentationMessage = '문서' + timerAlreadyDefinedExceptionMessage = '[타이머] {0}: 타이머가 이미 정의되어 있습니다.' + invalidPortExceptionMessage = '포트는 음수일 수 없습니다: {0}' + viewsFolderNameAlreadyExistsExceptionMessage = '뷰 폴더 이름이 이미 존재합니다: {0}' + noNameForWebSocketResetExceptionMessage = '재설정할 WebSocket의 이름이 제공되지 않았습니다.' + mergeDefaultAuthNotInListExceptionMessage = "병합 기본 인증 '{0}'이(가) 제공된 인증 목록에 없습니다." + descriptionRequiredExceptionMessage = '경로:{0} 응답:{1} 에 대한 설명이 필요합니다' + pageNameShouldBeAlphaNumericExceptionMessage = '페이지 이름은 유효한 알파벳 숫자 값이어야 합니다: {0}' + defaultValueNotBooleanOrEnumExceptionMessage = '기본값이 boolean이 아니며 enum에 속하지 않습니다.' + openApiComponentSchemaDoesNotExistExceptionMessage = 'OpenApi 구성 요소 스키마 {0}이(가) 존재하지 않습니다.' + timerParameterMustBeGreaterThanZeroExceptionMessage = '[타이머] {0}: {1}은(는) 0보다 커야 합니다.' + taskTimedOutExceptionMessage = '작업이 {0}ms 후에 시간 초과되었습니다.' + scheduleStartTimeAfterEndTimeExceptionMessage = "[스케줄] {0}: 'StartTime'이 'EndTime' 이후일 수 없습니다." + infoVersionMandatoryMessage = 'info.version은 필수 항목입니다.' + cannotUnlockNullObjectExceptionMessage = 'null 개체를 잠금 해제할 수 없습니다.' + nonEmptyScriptBlockRequiredForCustomAuthExceptionMessage = '사용자 정의 인증 스킴에는 비어 있지 않은 ScriptBlock이 필요합니다.' + nonEmptyScriptBlockRequiredForAuthMethodExceptionMessage = '인증 방법에 대해 비어 있지 않은 ScriptBlock이 필요합니다.' + validationOfOneOfSchemaNotSupportedExceptionMessage = "'oneof'을 포함하는 스키마의 유효성 검사는 지원되지 않습니다." + routeParameterCannotBeNullExceptionMessage = "'Route' 매개변수는 null일 수 없습니다." + cacheStorageAlreadyExistsExceptionMessage = "이름이 '{0}'인 캐시 스토리지가 이미 존재합니다." + loggingMethodRequiresValidScriptBlockExceptionMessage = "'{0}' 로깅 방법에 대한 제공된 출력 방법은 유효한 ScriptBlock이 필요합니다." + scopedVariableAlreadyDefinedExceptionMessage = '범위 지정 변수가 이미 정의되었습니다: {0}' + oauth2RequiresAuthorizeUrlExceptionMessage = 'OAuth2에는 권한 부여 URL이 필요합니다.' + pathNotExistExceptionMessage = '경로가 존재하지 않습니다: {0}' + noDomainServerNameForWindowsAdAuthExceptionMessage = 'Windows AD 인증을 위한 도메인 서버 이름이 제공되지 않았습니다.' + suppliedDateAfterScheduleEndTimeExceptionMessage = '제공된 날짜가 스케줄 종료 시간 {0} 이후입니다.' + wildcardMethodsIncompatibleWithAutoMethodsExceptionMessage = '메서드에 대한 * 와일드카드는 AutoMethods 스위치와 호환되지 않습니다.' + cannotSupplyIntervalForYearExceptionMessage = '매년 간격 값을 제공할 수 없습니다.' + missingComponentsMessage = '누락된 구성 요소' + invalidStrictTransportSecurityDurationExceptionMessage = '잘못된 Strict-Transport-Security 기간이 제공되었습니다: {0}. 0보다 커야 합니다.' + noSecretForHmac512ExceptionMessage = 'HMAC512 해시를 위한 비밀이 제공되지 않았습니다.' + daysInMonthExceededExceptionMessage = '{0}에는 {1}일밖에 없지만 {2}일이 제공되었습니다.' + nonEmptyScriptBlockRequiredForCustomLoggingExceptionMessage = '사용자 정의 로깅 출력 방법에는 비어 있지 않은 ScriptBlock이 필요합니다.' + encodingAttributeOnlyAppliesToMultipartExceptionMessage = '인코딩 속성은 multipart 및 application/x-www-form-urlencoded 요청 본문에만 적용됩니다.' + suppliedDateBeforeScheduleStartTimeExceptionMessage = '제공된 날짜가 스케줄 시작 시간 {0} 이전입니다.' + unlockSecretRequiredExceptionMessage = "Microsoft.PowerShell.SecretStore를 사용할 때 'UnlockSecret' 속성이 필요합니다." + noLogicPassedForMethodRouteExceptionMessage = '[{0}] {1}: 논리가 전달되지 않았습니다.' + bodyParserAlreadyDefinedForContentTypeExceptionMessage = '{0} 콘텐츠 유형에 대한 바디 파서가 이미 정의되어 있습니다.' + invalidJwtSuppliedExceptionMessage = '제공된 JWT가 유효하지 않습니다.' + sessionsRequiredForFlashMessagesExceptionMessage = '플래시 메시지를 사용하려면 세션이 필요합니다.' + semaphoreAlreadyExistsExceptionMessage = "이름이 '{0}'인 세마포어가 이미 존재합니다." + invalidJwtHeaderAlgorithmSuppliedExceptionMessage = '제공된 JWT 헤더 알고리즘이 유효하지 않습니다.' + oauth2ProviderDoesNotSupportPasswordGrantTypeExceptionMessage = "OAuth2 공급자는 InnerScheme을 사용하는 데 필요한 'password' 부여 유형을 지원하지 않습니다." + invalidAliasFoundExceptionMessage = '잘못된 {0} 별칭이 발견되었습니다: {1}' + scheduleDoesNotExistExceptionMessage = "스케줄 '{0}'이(가) 존재하지 않습니다." + accessMethodNotExistExceptionMessage = '액세스 방법이 존재하지 않습니다: {0}' + oauth2ProviderDoesNotSupportCodeResponseTypeExceptionMessage = "OAuth2 공급자는 'code' 응답 유형을 지원하지 않습니다." + untestedPowerShellVersionWarningMessage = '[경고] Pode {0}은 출시 당시 사용 가능하지 않았기 때문에 PowerShell {1}에서 테스트되지 않았습니다.' + secretVaultAlreadyRegisteredAutoImportExceptionMessage = "이름이 '{0}'인 비밀 금고가 이미 자동으로 가져오는 동안 등록되었습니다." + schemeRequiresValidScriptBlockExceptionMessage = "'{0}' 인증 검증기에 제공된 스킴에는 유효한 ScriptBlock이 필요합니다." + serverLoopingMessage = '서버 루핑 간격 {0}초' + certificateThumbprintsNameSupportedOnWindowsExceptionMessage = '인증서 지문/이름은 Windows에서만 지원됩니다.' + sseConnectionNameRequiredExceptionMessage = "-Name 또는 `$WebEvent.Sse.Name에서 SSE 연결 이름이 필요합니다." + invalidMiddlewareTypeExceptionMessage = '제공된 미들웨어 중 하나가 잘못된 유형입니다. 예상된 유형은 ScriptBlock 또는 Hashtable이지만, 얻은 것은: {0}' + noSecretForJwtSignatureExceptionMessage = 'JWT 서명을 위한 비밀이 제공되지 않았습니다.' + modulePathDoesNotExistExceptionMessage = '모듈 경로가 존재하지 않습니다: {0}' + taskAlreadyDefinedExceptionMessage = '[작업] {0}: 작업이 이미 정의되었습니다.' + verbAlreadyDefinedExceptionMessage = '[동사] {0}: 이미 정의되었습니다.' + clientCertificatesOnlySupportedOnHttpsEndpointsExceptionMessage = '클라이언트 인증서는 HTTPS 엔드포인트에서만 지원됩니다.' + endpointNameNotExistExceptionMessage = "이름이 '{0}'인 엔드포인트가 존재하지 않습니다." + middlewareNoLogicSuppliedExceptionMessage = '[미들웨어]: ScriptBlock에 로직이 제공되지 않았습니다.' + scriptBlockRequiredForMergingUsersExceptionMessage = 'Valid가 All일 때 여러 인증된 사용자를 하나의 객체로 병합하려면 ScriptBlock이 필요합니다.' + secretVaultAlreadyRegisteredExceptionMessage = "이름이 '{0}'인 시크릿 금고가 이미 등록되었습니다{1}." + deprecatedTitleVersionDescriptionWarningMessage = "경고: 'Enable-PodeOpenApi'의 제목, 버전 및 설명이 더 이상 사용되지 않습니다. 대신 'Add-PodeOAInfo'를 사용하십시오." + undefinedOpenApiReferencesMessage = '정의되지 않은 OpenAPI 참조:' + doneMessage = '완료' + swaggerEditorDoesNotSupportOpenApi31ExceptionMessage = '이 버전의 Swagger-Editor는 OpenAPI 3.1을 지원하지 않습니다.' + durationMustBeZeroOrGreaterExceptionMessage = '기간은 0 이상이어야 하지만 받은 값: {0}s' + viewsPathDoesNotExistExceptionMessage = '뷰 경로가 존재하지 않습니다: {0}' + discriminatorIncompatibleWithAllOfExceptionMessage = "매개변수 'Discriminator'는 'allOf'와 호환되지 않습니다." + noNameForWebSocketSendMessageExceptionMessage = '메시지를 보낼 WebSocket의 이름이 제공되지 않았습니다.' + hashtableMiddlewareNoLogicExceptionMessage = '제공된 Hashtable 미들웨어에는 정의된 논리가 없습니다.' + openApiInfoMessage = 'OpenAPI 정보:' + invalidSchemeForAuthValidatorExceptionMessage = "'{1}' 인증 검증기에 제공된 '{0}' 스킴에는 유효한 ScriptBlock이 필요합니다." + sseFailedToBroadcastExceptionMessage = '{0}에 대해 정의된 SSE 브로드캐스트 수준으로 인해 SSE 브로드캐스트에 실패했습니다: {1}' + adModuleWindowsOnlyExceptionMessage = 'Active Directory 모듈은 Windows에서만 사용할 수 있습니다.' + requestLoggingAlreadyEnabledExceptionMessage = '요청 로깅이 이미 활성화되었습니다.' + invalidAccessControlMaxAgeDurationExceptionMessage = '잘못된 Access-Control-Max-Age 기간이 제공되었습니다: {0}. 0보다 커야 합니다.' + openApiDefinitionAlreadyExistsExceptionMessage = '이름이 {0}인 OpenAPI 정의가 이미 존재합니다.' + renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag은 Select-PodeOADefinition 'ScriptBlock' 내에서 사용할 수 없습니다." + taskProcessDoesNotExistExceptionMessage = '작업 프로세스가 존재하지 않습니다: {0}' + scheduleProcessDoesNotExistExceptionMessage = '스케줄 프로세스가 존재하지 않습니다: {0}' + definitionTagChangeNotAllowedExceptionMessage = 'Route에 대한 정의 태그는 변경할 수 없습니다.' + getRequestBodyNotAllowedExceptionMessage = '{0} 작업에는 요청 본문이 있을 수 없습니다.' + fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "함수 '{0}'은(는) 배열을 파이프라인 입력으로 받지 않습니다." + unsupportedStreamCompressionEncodingExceptionMessage = '지원되지 않는 스트림 압축 인코딩: {0}' +} \ No newline at end of file diff --git a/src/Locales/nl/Pode.psd1 b/src/Locales/nl/Pode.psd1 new file mode 100644 index 000000000..8e88fe7d5 --- /dev/null +++ b/src/Locales/nl/Pode.psd1 @@ -0,0 +1,294 @@ +@{ + schemaValidationRequiresPowerShell610ExceptionMessage = 'Schema-validatie vereist PowerShell versie 6.1.0 of hoger.' + customAccessPathOrScriptBlockRequiredExceptionMessage = 'Een pad of ScriptBlock is vereist voor het verkrijgen van de aangepaste toegangswaarden.' + operationIdMustBeUniqueForArrayExceptionMessage = 'OperationID: {0} moet uniek zijn en kan niet worden toegepast op een array.' + endpointNotDefinedForRedirectingExceptionMessage = "Er is geen eindpunt met de naam '{0}' gedefinieerd voor omleiding." + filesHaveChangedMessage = 'De volgende bestanden zijn gewijzigd:' + iisAspnetcoreTokenMissingExceptionMessage = 'IIS ASPNETCORE_TOKEN ontbreekt.' + minValueGreaterThanMaxExceptionMessage = 'Min waarde voor {0} mag niet groter zijn dan de max waarde.' + noLogicPassedForRouteExceptionMessage = 'Geen logica doorgegeven voor Route: {0}' + scriptPathDoesNotExistExceptionMessage = 'Het scriptpad bestaat niet: {0}' + mutexAlreadyExistsExceptionMessage = 'Er bestaat al een mutex met de volgende naam: {0}' + listeningOnEndpointsMessage = 'Luisteren naar de volgende {0} eindpunt(en) [{1} thread(s)]:' + unsupportedFunctionInServerlessContextExceptionMessage = 'De functie {0} wordt niet ondersteund in een serverloze context.' + expectedNoJwtSignatureSuppliedExceptionMessage = 'Er werd geen JWT-handtekening verwacht.' + secretAlreadyMountedExceptionMessage = "Er is al een geheim met de naam '{0}' gemonteerd." + failedToAcquireLockExceptionMessage = 'Kan geen lock op het object verkrijgen.' + noPathSuppliedForStaticRouteExceptionMessage = '[{0}]: Geen pad opgegeven voor statische route.' + invalidHostnameSuppliedExceptionMessage = 'Ongeldige hostnaam opgegeven: {0}' + authMethodAlreadyDefinedExceptionMessage = 'Authenticatiemethode al gedefinieerd: {0}' + csrfCookieRequiresSecretExceptionMessage = "Bij gebruik van cookies voor CSRF is een geheim vereist. U kunt een geheim opgeven of het globale cookiesecret instellen - (Set-PodeCookieSecret '' -Global)" + nonEmptyScriptBlockRequiredForAuthMethodExceptionMessage = 'Een niet-lege ScriptBlock is vereist voor de authenticatiemethode.' + nonEmptyScriptBlockRequiredForPageRouteExceptionMessage = 'Een niet-lege ScriptBlock is vereist om een paginaroute te maken.' + noPropertiesMutuallyExclusiveExceptionMessage = "De parameter 'NoProperties' is wederzijds exclusief met 'Properties', 'MinProperties' en 'MaxProperties'" + incompatiblePodeDllExceptionMessage = 'Een bestaande incompatibele Pode.DLL-versie {0} is geladen. Versie {1} is vereist. Open een nieuwe PowerShell/pwsh-sessie en probeer opnieuw.' + accessMethodDoesNotExistExceptionMessage = 'Toegangsmethode bestaat niet: {0}.' + scheduleAlreadyDefinedExceptionMessage = '[Schema] {0}: Schema al gedefinieerd.' + secondsValueCannotBeZeroOrLessExceptionMessage = 'Waarde in seconden kan niet 0 of minder zijn voor {0}' + pathToLoadNotFoundExceptionMessage = 'Pad om te laden {0} niet gevonden: {1}' + failedToImportModuleExceptionMessage = 'Kon module niet importeren: {0}' + endpointNotExistExceptionMessage = "Eindpunt met protocol '{0}' en adres '{1}' of lokaal adres '{2}' bestaat niet." + terminatingMessage = 'Beëindigen...' + noCommandsSuppliedToConvertToRoutesExceptionMessage = 'Geen opdrachten opgegeven om om te zetten naar routes.' + invalidTaskTypeExceptionMessage = 'Taaktype is ongeldig, verwacht ofwel [System.Threading.Tasks.Task] of [hashtable]' + alreadyConnectedToWebSocketExceptionMessage = "Al verbonden met WebSocket met naam '{0}'" + crlfMessageEndCheckOnlySupportedOnTcpEndpointsExceptionMessage = 'De CRLF-berichteneindcontrole wordt alleen ondersteund op TCP-eindpunten.' + testPodeOAComponentSchemaNeedToBeEnabledExceptionMessage = "'Test-PodeOAComponentSchema' moet worden ingeschakeld met 'Enable-PodeOpenApi -EnableSchemaValidation'" + adModuleNotInstalledExceptionMessage = 'Active Directory-module is niet geïnstalleerd.' + cronExpressionInvalidExceptionMessage = 'Cron-expressie mag alleen uit 5 delen bestaan: {0}' + noSessionToSetOnResponseExceptionMessage = 'Er is geen sessie beschikbaar om op de reactie in te stellen.' + valueOutOfRangeExceptionMessage = "Waarde '{0}' voor {1} is ongeldig, moet tussen {2} en {3} liggen" + loggingMethodAlreadyDefinedExceptionMessage = 'De logboekmethode is al gedefinieerd: {0}' + noSecretForHmac256ExceptionMessage = 'Geen geheim opgegeven voor HMAC256-hash.' + eolPowerShellWarningMessage = '[WAARSCHUWING] Pode {0} is niet getest op PowerShell {1}, omdat het EOL is.' + runspacePoolFailedToLoadExceptionMessage = '{0} RunspacePool kon niet geladen worden.' + noEventRegisteredExceptionMessage = 'Geen {0} gebeurtenis geregistreerd: {1}' + scheduleCannotHaveNegativeLimitExceptionMessage = '[Schema] {0}: Kan geen negatieve limiet hebben.' + openApiRequestStyleInvalidForParameterExceptionMessage = 'OpenApi-verzoekstijl kan niet {0} zijn voor een {1} parameter.' + openApiDocumentNotCompliantExceptionMessage = 'OpenAPI-document voldoet niet aan de normen.' + taskDoesNotExistExceptionMessage = "Taak '{0}' bestaat niet." + scopedVariableNotFoundExceptionMessage = 'Gescopede variabele niet gevonden: {0}' + sessionsRequiredForCsrfExceptionMessage = 'Sessies zijn vereist om CSRF te gebruiken, tenzij u cookies wilt gebruiken.' + nonEmptyScriptBlockRequiredForLoggingMethodExceptionMessage = 'Een niet-lege ScriptBlock is vereist voor de logboekmethode.' + credentialsPassedWildcardForHeadersLiteralExceptionMessage = 'Wanneer referenties worden doorgegeven, wordt het * jokerteken voor headers als een letterlijke tekenreeks en niet als een jokerteken genomen.' + podeNotInitializedExceptionMessage = 'Pode is niet geïnitialiseerd.' + multipleEndpointsForGuiMessage = 'Meerdere eindpunten gedefinieerd, alleen het eerste wordt gebruikt voor de GUI.' + operationIdMustBeUniqueExceptionMessage = 'OperationID: {0} moet uniek zijn.' + invalidJsonJwtExceptionMessage = 'Ongeldige JSON-waarde gevonden in JWT' + noAlgorithmInJwtHeaderExceptionMessage = 'Geen algoritme opgegeven in JWT-header.' + openApiVersionPropertyMandatoryExceptionMessage = 'OpenApi-versie-eigenschap is verplicht.' + limitValueCannotBeZeroOrLessExceptionMessage = 'Limietwaarde kan niet 0 of minder zijn voor {0}' + timerDoesNotExistExceptionMessage = "Timer '{0}' bestaat niet." + openApiGenerationDocumentErrorMessage = 'OpenAPI-generatiedocumentfout:' + routeAlreadyContainsCustomAccessExceptionMessage = "Route '[{0}] {1}' bevat al aangepaste toegang met naam '{2}'" + maximumConcurrentWebSocketThreadsLessThanMinimumExceptionMessage = 'Maximaal aantal gelijktijdige WebSocket-threads kan niet minder zijn dan het minimum van {0} maar kreeg: {1}' + middlewareAlreadyDefinedExceptionMessage = '[Middleware] {0}: Middleware al gedefinieerd.' + invalidAtomCharacterExceptionMessage = 'Ongeldig atoomteken: {0}' + invalidCronAtomFormatExceptionMessage = 'Ongeldig cron-atoomformaat gevonden: {0}' + cacheStorageNotFoundForRetrieveExceptionMessage = "Cache-opslag met naam '{0}' niet gevonden bij poging om gecachte item '{1}' op te halen" + headerMustHaveNameInEncodingContextExceptionMessage = 'Header moet een naam hebben wanneer deze in een coderingscontext wordt gebruikt.' + moduleDoesNotContainFunctionExceptionMessage = 'Module {0} bevat geen functie {1} om om te zetten naar een route.' + pathToIconForGuiDoesNotExistExceptionMessage = 'Pad naar het pictogram voor GUI bestaat niet: {0}' + noTitleSuppliedForPageExceptionMessage = 'Geen titel opgegeven voor {0} pagina.' + certificateSuppliedForNonHttpsWssEndpointExceptionMessage = 'Certificaat opgegeven voor niet-HTTPS/WSS-eindpunt.' + cannotLockNullObjectExceptionMessage = 'Kan geen object vergrendelen dat null is.' + showPodeGuiOnlyAvailableOnWindowsExceptionMessage = 'Show-PodeGui is momenteel alleen beschikbaar voor Windows PowerShell en PowerShell 7+ op Windows OS.' + unlockSecretButNoScriptBlockExceptionMessage = 'Ontgrendel geheim opgegeven voor aangepast geheimenkluis type, maar geen ontgrendel ScriptBlock opgegeven.' + invalidIpAddressExceptionMessage = 'Het opgegeven IP-adres is ongeldig: {0}' + maxDaysInvalidExceptionMessage = 'MaxDays moet 0 of groter zijn, maar kreeg: {0}' + noRemoveScriptBlockForVaultExceptionMessage = "Geen verwijder ScriptBlock opgegeven voor het verwijderen van geheimen uit de kluis '{0}'" + noSecretExpectedForNoSignatureExceptionMessage = 'Er werd geen geheim verwacht voor geen handtekening.' + noCertificateFoundExceptionMessage = "Geen certificaat gevonden in {0}{1} voor '{2}'" + minValueInvalidExceptionMessage = "Min waarde '{0}' voor {1} is ongeldig, moet groter zijn dan/gelijk aan {2}" + accessRequiresAuthenticationOnRoutesExceptionMessage = 'Toegang vereist dat authenticatie wordt opgegeven op routes.' + noSecretForHmac384ExceptionMessage = 'Geen geheim opgegeven voor HMAC384-hash.' + windowsLocalAuthSupportIsForWindowsOnlyExceptionMessage = 'Windows lokale authenticatie-ondersteuning is alleen voor Windows OS.' + definitionTagNotDefinedExceptionMessage = 'DefinitionTag {0} bestaat niet.' + noComponentInDefinitionExceptionMessage = 'Geen component van het type {0} genaamd {1} is beschikbaar in de {2} definitie.' + noSmtpHandlersDefinedExceptionMessage = 'Er zijn geen SMTP-handlers gedefinieerd.' + sessionMiddlewareAlreadyInitializedExceptionMessage = 'Sessie Middleware is al geïnitialiseerd.' + reusableComponentPathItemsNotAvailableInOpenApi30ExceptionMessage = "De herbruikbare componentfunctie 'pathItems' is niet beschikbaar in OpenAPI v3.0." + wildcardHeadersIncompatibleWithAutoHeadersExceptionMessage = 'Het * jokerteken voor headers is niet compatibel met de AutoHeaders-schakelaar.' + noDataForFileUploadedExceptionMessage = "Geen gegevens voor bestand '{0}' zijn geüpload in het verzoek." + sseOnlyConfiguredOnEventStreamAcceptHeaderExceptionMessage = 'SSE kan alleen worden geconfigureerd voor verzoeken met een Accept-headerwaarde van text/event-stream' + noSessionAvailableToSaveExceptionMessage = 'Er is geen sessie beschikbaar om op te slaan.' + pathParameterRequiresRequiredSwitchExceptionMessage = "Als de parameterlocatie 'Pad' is, is de schakelparameter 'Vereist' verplicht." + noOpenApiUrlSuppliedExceptionMessage = 'Geen OpenAPI-URL opgegeven voor {0}.' + maximumConcurrentSchedulesInvalidExceptionMessage = "Maximaal aantal gelijktijdige schema's moet >=1 zijn, maar kreeg: {0}" + snapinsSupportedOnWindowsPowershellOnlyExceptionMessage = 'Snapins worden alleen ondersteund op Windows PowerShell.' + eventViewerLoggingSupportedOnWindowsOnlyExceptionMessage = 'Event Viewer-logboekregistratie wordt alleen ondersteund op Windows OS.' + parametersMutuallyExclusiveExceptionMessage = "Parameters '{0}' en '{1}' zijn wederzijds exclusief." + pathItemsFeatureNotSupportedInOpenApi30ExceptionMessage = 'De functie PathItems wordt niet ondersteund in OpenAPI v3.0.x' + openApiParameterRequiresNameExceptionMessage = 'De OpenApi-parameter vereist een naam om te worden opgegeven.' + maximumConcurrentTasksLessThanMinimumExceptionMessage = 'Maximaal aantal gelijktijdige taken kan niet minder zijn dan het minimum van {0} maar kreeg: {1}' + noSemaphoreFoundExceptionMessage = "Geen semafoor gevonden genaamd '{0}'" + singleValueForIntervalExceptionMessage = 'U kunt slechts één {0} waarde opgeven bij gebruik van intervallen.' + jwtNotYetValidExceptionMessage = 'De JWT is nog niet geldig voor gebruik.' + verbAlreadyDefinedForUrlExceptionMessage = '[Werkwoord] {0}: Al gedefinieerd voor {1}' + noSecretNamedMountedExceptionMessage = "Geen geheim genaamd '{0}' is gemonteerd." + moduleOrVersionNotFoundExceptionMessage = 'Module of versie niet gevonden op {0}: {1}@{2}' + noScriptBlockSuppliedExceptionMessage = 'Geen ScriptBlock opgegeven.' + noSecretVaultRegisteredExceptionMessage = "Geen geheime kluis met de naam '{0}' is geregistreerd." + nameRequiredForEndpointIfRedirectToSuppliedExceptionMessage = 'Een naam is vereist voor het eindpunt als de parameter RedirectTo is opgegeven.' + openApiLicenseObjectRequiresNameExceptionMessage = "Het OpenAPI-object 'licentie' vereist de eigenschap 'naam'. Gebruik de parameter -LicenseName." + sourcePathDoesNotExistForStaticRouteExceptionMessage = '{0}: Het opgegeven bronpad voor statische route bestaat niet: {1}' + noNameForWebSocketDisconnectExceptionMessage = 'Geen naam opgegeven voor een WebSocket om los te koppelen.' + certificateExpiredExceptionMessage = "Het certificaat '{0}' is verlopen: {1}" + secretVaultUnlockExpiryDateInPastExceptionMessage = 'De ontgrendelingsvervaldatum van de geheime kluis ligt in het verleden (UTC): {0}' + invalidWebExceptionTypeExceptionMessage = 'Uitzondering is van een ongeldig type, moet ofwel WebException of HttpRequestException zijn, maar kreeg: {0}' + invalidSecretValueTypeExceptionMessage = 'Geheime waarde is van een ongeldig type. Verwachte types: String, SecureString, HashTable, Byte[], of PSCredential. Maar kreeg: {0}' + explicitTlsModeOnlySupportedOnSmtpsTcpsEndpointsExceptionMessage = 'De expliciete TLS-modus wordt alleen ondersteund op SMTPS- en TCPS-eindpunten.' + discriminatorMappingRequiresDiscriminatorPropertyExceptionMessage = "De parameter 'DiscriminatorMapping' kan alleen worden gebruikt wanneer 'DiscriminatorProperty' aanwezig is." + scriptErrorExceptionMessage = "Fout '{0}' in script {1} {2} (regel {3}) teken {4} bij uitvoeren {5} op {6} object '{7}' Klasse: {8} BasisKlasse: {9}" + cannotSupplyIntervalForQuarterExceptionMessage = 'Kan geen intervalwaarde opgeven voor elk kwartaal.' + scheduleEndTimeMustBeInFutureExceptionMessage = '[Schema] {0}: De eindtijdwaarde moet in de toekomst liggen.' + invalidJwtSignatureSuppliedExceptionMessage = 'Ongeldige JWT-handtekening opgegeven.' + noSetScriptBlockForVaultExceptionMessage = "Geen Set ScriptBlock opgegeven voor het bijwerken/maken van geheimen in de kluis '{0}'" + accessMethodNotExistForMergingExceptionMessage = 'Toegangsmethode bestaat niet voor samenvoegen: {0}' + defaultAuthNotInListExceptionMessage = "De standaardauthenticatie '{0}' staat niet in de opgegeven authenticatielijst." + parameterHasNoNameExceptionMessage = "De parameter heeft geen naam. Geef dit component een naam met de parameter 'Naam'." + methodPathAlreadyDefinedForUrlExceptionMessage = '[{0}] {1}: Al gedefinieerd voor {2}' + fileWatcherAlreadyDefinedExceptionMessage = "Een bestand bewaker genaamd '{0}' is al gedefinieerd." + noServiceHandlersDefinedExceptionMessage = 'Er zijn geen servicehandlers gedefinieerd.' + secretRequiredForCustomSessionStorageExceptionMessage = 'Een geheim is vereist bij gebruik van aangepaste sessieopslag.' + secretManagementModuleNotInstalledExceptionMessage = 'Microsoft.PowerShell.SecretManagement module niet geïnstalleerd.' + noPathSuppliedForRouteExceptionMessage = 'Geen pad opgegeven voor de route.' + validationOfAnyOfSchemaNotSupportedExceptionMessage = "Validatie van een schema dat 'anyof' bevat, wordt niet ondersteund." + iisAuthSupportIsForWindowsOnlyExceptionMessage = 'IIS-authenticatieondersteuning is alleen voor Windows OS.' + oauth2InnerSchemeInvalidExceptionMessage = 'OAuth2 InnerScheme kan alleen een van de basale of formulierauthenticatie zijn, maar kreeg: {0}' + noRoutePathSuppliedForPageExceptionMessage = 'Geen routepad opgegeven voor {0} pagina.' + cacheStorageNotFoundForExistsExceptionMessage = "Cache-opslag met naam '{0}' niet gevonden bij poging om te controleren of gecachte item '{1}' bestaat." + handlerAlreadyDefinedExceptionMessage = '[{0}] {1}: Handler al gedefinieerd.' + sessionsNotConfiguredExceptionMessage = 'Sessies zijn niet geconfigureerd.' + propertiesTypeObjectAssociationExceptionMessage = 'Alleen eigenschappen van het type Object kunnen worden geassocieerd met {0}.' + sessionsRequiredForSessionPersistentAuthExceptionMessage = 'Sessies zijn vereist om sessie-persistente authenticatie te gebruiken.' + invalidPathWildcardOrDirectoryExceptionMessage = 'Het opgegeven pad kan geen wildcard of een directory zijn: {0}' + accessMethodAlreadyDefinedExceptionMessage = 'Toegangsmethode al gedefinieerd: {0}' + parametersValueOrExternalValueMandatoryExceptionMessage = "Parameters 'Value' of 'ExternalValue' zijn verplicht" + maximumConcurrentTasksInvalidExceptionMessage = 'Maximaal aantal gelijktijdige taken moet >=1 zijn, maar kreeg: {0}' + cannotCreatePropertyWithoutTypeExceptionMessage = 'Kan de eigenschap niet maken omdat er geen type is gedefinieerd.' + authMethodNotExistForMergingExceptionMessage = 'Authenticatiemethode bestaat niet voor samenvoegen: {0}' + maxValueInvalidExceptionMessage = "Max waarde '{0}' voor {1} is ongeldig, moet minder dan/gelijk aan {2} zijn" + endpointAlreadyDefinedExceptionMessage = "Er is al een eindpunt met de naam '{0}' gedefinieerd." + eventAlreadyRegisteredExceptionMessage = '{0} gebeurtenis al geregistreerd: {1}' + parameterNotSuppliedInRequestExceptionMessage = "Een parameter genaamd '{0}' is niet opgegeven in het verzoek of heeft geen beschikbare gegevens." + cacheStorageNotFoundForSetExceptionMessage = "Cache-opslag met naam '{0}' niet gevonden bij poging om gecachte item '{1}' in te stellen" + methodPathAlreadyDefinedExceptionMessage = '[{0}] {1}: Al gedefinieerd.' + errorLoggingAlreadyEnabledExceptionMessage = 'Foutlogboekregistratie is al ingeschakeld.' + valueForUsingVariableNotFoundExceptionMessage = "Waarde voor '`$using:{0}' kon niet worden gevonden." + rapidPdfDoesNotSupportOpenApi31ExceptionMessage = 'Het Document-tool RapidPdf ondersteunt OpenAPI 3.1 niet' + oauth2ClientSecretRequiredExceptionMessage = 'OAuth2 vereist een Client Secret wanneer PKCE niet wordt gebruikt.' + invalidBase64JwtExceptionMessage = 'Ongeldige Base64-gecodeerde waarde gevonden in JWT' + noSessionToCalculateDataHashExceptionMessage = 'Geen sessie beschikbaar om gegevenshash te berekenen.' + cacheStorageNotFoundForRemoveExceptionMessage = "Cache-opslag met naam '{0}' niet gevonden bij poging om gecachte item '{1}' te verwijderen" + csrfMiddlewareNotInitializedExceptionMessage = 'CSRF Middleware is niet geïnitialiseerd.' + infoTitleMandatoryMessage = 'info.title is verplicht.' + typeCanOnlyBeAssociatedWithObjectExceptionMessage = 'Type {0} kan alleen worden geassocieerd met een Object.' + userFileDoesNotExistExceptionMessage = 'Het gebruikersbestand bestaat niet: {0}' + routeParameterNeedsValidScriptblockExceptionMessage = 'De routeparameter heeft een geldige, niet-lege, scriptblock nodig.' + nextTriggerCalculationErrorExceptionMessage = 'Er lijkt iets mis te zijn gegaan bij het berekenen van de volgende triggerdatum: {0}' + cannotLockValueTypeExceptionMessage = 'Kan een [ValueType] niet vergrendelen' + failedToCreateOpenSslCertExceptionMessage = 'Kon OpenSSL-certificaat niet maken: {0}' + jwtExpiredExceptionMessage = 'De JWT is verlopen.' + openingGuiMessage = 'De GUI wordt geopend.' + multiTypePropertiesRequireOpenApi31ExceptionMessage = 'Multi-type eigenschappen vereisen OpenApi versie 3.1 of hoger.' + noNameForWebSocketRemoveExceptionMessage = 'Geen naam opgegeven voor een WebSocket om te verwijderen.' + maxSizeInvalidExceptionMessage = 'MaxSize moet 0 of groter zijn, maar kreeg: {0}' + iisShutdownMessage = '(IIS Afsluiting)' + cannotUnlockValueTypeExceptionMessage = 'Kan een [ValueType] niet ontgrendelen' + noJwtSignatureForAlgorithmExceptionMessage = 'Geen JWT-handtekening opgegeven voor {0}.' + maximumConcurrentWebSocketThreadsInvalidExceptionMessage = 'Maximaal aantal gelijktijdige WebSocket-threads moet >=1 zijn, maar kreeg: {0}' + acknowledgeMessageOnlySupportedOnSmtpTcpEndpointsExceptionMessage = 'Het Acknowledge-bericht wordt alleen ondersteund op SMTP- en TCP-eindpunten.' + failedToConnectToUrlExceptionMessage = 'Kon geen verbinding maken met URL: {0}' + failedToAcquireMutexOwnershipExceptionMessage = 'Kon geen mutex-eigendom verkrijgen. Mutex-naam: {0}' + sessionsRequiredForOAuth2WithPKCEExceptionMessage = 'Sessies zijn vereist om OAuth2 met PKCE te gebruiken' + failedToConnectToWebSocketExceptionMessage = 'Kon geen verbinding maken met WebSocket: {0}' + unsupportedObjectExceptionMessage = 'Niet ondersteund object' + failedToParseAddressExceptionMessage = "Kon '{0}' niet parseren als een geldig IP/Host:Port adres" + mustBeRunningWithAdminPrivilegesExceptionMessage = 'Moet worden uitgevoerd met beheerdersrechten om naar niet-lokale adressen te luisteren.' + specificationMessage = 'Specificatie' + cacheStorageNotFoundForClearExceptionMessage = "Cache-opslag met naam '{0}' niet gevonden bij poging om de cache te wissen." + restartingServerMessage = 'Server opnieuw starten...' + cannotSupplyIntervalWhenEveryIsNoneExceptionMessage = "Kan geen interval opgeven wanneer de parameter 'Every' is ingesteld op None." + unsupportedJwtAlgorithmExceptionMessage = 'Het JWT-algoritme wordt momenteel niet ondersteund: {0}' + websocketsNotConfiguredForSignalMessagesExceptionMessage = 'WebSockets zijn niet geconfigureerd om signaalberichten te verzenden.' + invalidLogicTypeInHashtableMiddlewareExceptionMessage = 'Een opgegeven Hashtable Middleware heeft een ongeldig logica-type. Verwachte ScriptBlock, maar kreeg: {0}' + maximumConcurrentSchedulesLessThanMinimumExceptionMessage = "Maximaal aantal gelijktijdige schema's kan niet minder zijn dan het minimum van {0} maar kreeg: {1}" + failedToAcquireSemaphoreOwnershipExceptionMessage = 'Kon geen semafoor-eigendom verkrijgen. Semafoornaam: {0}' + propertiesParameterWithoutNameExceptionMessage = 'De eigenschappenparameters kunnen niet worden gebruikt als de eigenschap geen naam heeft.' + customSessionStorageMethodNotImplementedExceptionMessage = "De aangepaste sessieopslag implementeert de vereiste methode '{0}()' niet." + authenticationMethodDoesNotExistExceptionMessage = 'Authenticatiemethode bestaat niet: {0}' + webhooksFeatureNotSupportedInOpenApi30ExceptionMessage = 'De Webhooks-functie wordt niet ondersteund in OpenAPI v3.0.x' + invalidContentTypeForSchemaExceptionMessage = "Ongeldige 'content-type' gevonden voor schema: {0}" + noUnlockScriptBlockForVaultExceptionMessage = "Geen ontgrendel ScriptBlock opgegeven voor het ontgrendelen van de kluis '{0}'" + definitionTagMessage = 'Definitie {0}:' + failedToOpenRunspacePoolExceptionMessage = 'Kon RunspacePool niet openen: {0}' + failedToCloseRunspacePoolExceptionMessage = 'Kon RunspacePool niet sluiten: {0}' + verbNoLogicPassedExceptionMessage = '[Werkwoord] {0}: Geen logica doorgegeven' + noMutexFoundExceptionMessage = "Geen mutex gevonden genaamd '{0}'" + documentationMessage = 'Documentatie' + timerAlreadyDefinedExceptionMessage = '[Timer] {0}: Timer al gedefinieerd.' + invalidPortExceptionMessage = 'De poort kan niet negatief zijn: {0}' + viewsFolderNameAlreadyExistsExceptionMessage = 'De mapnaam Views bestaat al: {0}' + noNameForWebSocketResetExceptionMessage = 'Geen naam opgegeven voor een WebSocket om te resetten.' + mergeDefaultAuthNotInListExceptionMessage = "De standaardauthenticatie '{0}' staat niet in de opgegeven authenticatielijst." + descriptionRequiredExceptionMessage = 'Een beschrijving is vereist voor Pad:{0} Antwoord:{1}' + pageNameShouldBeAlphaNumericExceptionMessage = 'De paginanaam moet een geldige alfanumerieke waarde zijn: {0}' + defaultValueNotBooleanOrEnumExceptionMessage = 'De standaardwaarde is geen boolean en maakt geen deel uit van de enum.' + openApiComponentSchemaDoesNotExistExceptionMessage = 'Het OpenApi-component schema {0} bestaat niet.' + timerParameterMustBeGreaterThanZeroExceptionMessage = '[Timer] {0}: {1} moet groter zijn dan 0.' + taskTimedOutExceptionMessage = 'Taak is verlopen na {0}ms.' + scheduleStartTimeAfterEndTimeExceptionMessage = '[Schema] {0}: Kan geen StartTime hebben na de EndTime' + infoVersionMandatoryMessage = 'info.version is verplicht.' + cannotUnlockNullObjectExceptionMessage = 'Kan een object dat null is niet ontgrendelen.' + nonEmptyScriptBlockRequiredForCustomAuthExceptionMessage = 'Een niet-lege ScriptBlock is vereist voor het aangepaste authenticatieschema.' + validationOfOneOfSchemaNotSupportedExceptionMessage = "Validatie van een schema dat 'oneof' bevat, wordt niet ondersteund." + routeParameterCannotBeNullExceptionMessage = "De parameter 'Route' kan niet null zijn." + cacheStorageAlreadyExistsExceptionMessage = "Cache-opslag met naam '{0}' bestaat al." + loggingMethodRequiresValidScriptBlockExceptionMessage = "De opgegeven uitvoeringsmethode voor de '{0}' logboekmethode vereist een geldige ScriptBlock." + scopedVariableAlreadyDefinedExceptionMessage = 'Gescopede variabele al gedefinieerd: {0}' + oauth2RequiresAuthorizeUrlExceptionMessage = "OAuth2 vereist een 'AuthoriseUrl'-eigenschap om te worden opgegeven." + pathNotExistExceptionMessage = 'Pad bestaat niet: {0}' + noDomainServerNameForWindowsAdAuthExceptionMessage = 'Er is geen domeinservernaam opgegeven voor Windows AD-authenticatie' + suppliedDateAfterScheduleEndTimeExceptionMessage = 'Opgegeven datum is na de eindtijd van het schema op {0}' + wildcardMethodsIncompatibleWithAutoMethodsExceptionMessage = 'Het * jokerteken voor methoden is niet compatibel met de AutoMethods-schakelaar.' + cannotSupplyIntervalForYearExceptionMessage = 'Kan geen intervalwaarde opgeven voor elk jaar.' + missingComponentsMessage = 'Ontbrekende component(en)' + invalidStrictTransportSecurityDurationExceptionMessage = 'Ongeldige Strict-Transport-Security duur opgegeven: {0}. Het moet groter zijn dan 0.' + noSecretForHmac512ExceptionMessage = 'Geen geheim opgegeven voor HMAC512-hash.' + daysInMonthExceededExceptionMessage = '{0} heeft slechts {1} dagen, maar {2} is opgegeven.' + nonEmptyScriptBlockRequiredForCustomLoggingExceptionMessage = 'Een niet-lege ScriptBlock is vereist voor de aangepaste logboekuitvoermethode.' + encodingAttributeOnlyAppliesToMultipartExceptionMessage = 'Het coderingsattribuut is alleen van toepassing op multipart en application/x-www-form-urlencoded request bodies.' + suppliedDateBeforeScheduleStartTimeExceptionMessage = 'Opgegeven datum is voor de starttijd van het schema op {0}' + unlockSecretRequiredExceptionMessage = "Een 'UnlockSecret' eigenschap is vereist bij gebruik van Microsoft.PowerShell.SecretStore" + noLogicPassedForMethodRouteExceptionMessage = '[{0}] {1}: Geen logica doorgegeven.' + bodyParserAlreadyDefinedForContentTypeExceptionMessage = 'Er is al een body-parser gedefinieerd voor de {0} content-type.' + invalidJwtSuppliedExceptionMessage = 'Ongeldige JWT opgegeven.' + sessionsRequiredForFlashMessagesExceptionMessage = 'Sessies zijn vereist om Flash-berichten te gebruiken.' + semaphoreAlreadyExistsExceptionMessage = 'Een semafoor met de volgende naam bestaat al: {0}' + invalidJwtHeaderAlgorithmSuppliedExceptionMessage = 'Ongeldig JWT-headeralgoritme opgegeven.' + oauth2ProviderDoesNotSupportPasswordGrantTypeExceptionMessage = "De OAuth2-provider ondersteunt het 'password' grant_type vereist door gebruik van een InnerScheme niet." + invalidAliasFoundExceptionMessage = 'Ongeldige {0} alias gevonden: {1}' + scheduleDoesNotExistExceptionMessage = "Schema '{0}' bestaat niet." + accessMethodNotExistExceptionMessage = 'Toegangsmethode bestaat niet: {0}' + oauth2ProviderDoesNotSupportCodeResponseTypeExceptionMessage = "De OAuth2-provider ondersteunt het 'code' response_type niet." + untestedPowerShellVersionWarningMessage = '[WAARSCHUWING] Pode {0} is niet getest op PowerShell {1}, omdat het niet beschikbaar was toen Pode werd uitgebracht.' + secretVaultAlreadyRegisteredAutoImportExceptionMessage = "Een geheime kluis met de naam '{0}' is al geregistreerd tijdens het automatisch importeren van geheime kluizen." + schemeRequiresValidScriptBlockExceptionMessage = "Het opgegeven schema voor de '{0}' authenticatievalidator vereist een geldige ScriptBlock." + serverLoopingMessage = 'Server loop elke {0} seconden' + certificateThumbprintsNameSupportedOnWindowsExceptionMessage = 'Certificaat thumbprints/naam worden alleen ondersteund op Windows OS.' + sseConnectionNameRequiredExceptionMessage = "Een SSE-verbindingnaam is vereist, hetzij van -Naam of `$WebEvent.Sse.Name" + invalidMiddlewareTypeExceptionMessage = 'Een van de opgegeven middlewares is van een ongeldig type. Verwachte ScriptBlock of Hashtable, maar kreeg: {0}' + noSecretForJwtSignatureExceptionMessage = 'Geen geheim opgegeven voor JWT-handtekening.' + modulePathDoesNotExistExceptionMessage = 'Het modulepad bestaat niet: {0}' + taskAlreadyDefinedExceptionMessage = '[Taak] {0}: Taak al gedefinieerd.' + verbAlreadyDefinedExceptionMessage = '[Werkwoord] {0}: Al gedefinieerd' + clientCertificatesOnlySupportedOnHttpsEndpointsExceptionMessage = 'Clientcertificaten worden alleen ondersteund op HTTPS-eindpunten.' + endpointNameNotExistExceptionMessage = "Eindpunt met naam '{0}' bestaat niet." + middlewareNoLogicSuppliedExceptionMessage = '[Middleware]: Geen logica opgegeven in ScriptBlock.' + scriptBlockRequiredForMergingUsersExceptionMessage = 'Een ScriptBlock voor het samenvoegen van meerdere geauthenticeerde gebruikers in één object is vereist wanneer Valid All is.' + secretVaultAlreadyRegisteredExceptionMessage = "Een geheime kluis met de naam '{0}' is al geregistreerd{1}." + deprecatedTitleVersionDescriptionWarningMessage = "WAARSCHUWING: Titel, versie en beschrijving op 'Enable-PodeOpenApi' zijn verouderd. Gebruik in plaats daarvan 'Add-PodeOAInfo'." + undefinedOpenApiReferencesMessage = 'Ongedefinieerde OpenAPI-referenties:' + doneMessage = 'Klaar' + swaggerEditorDoesNotSupportOpenApi31ExceptionMessage = 'Deze versie van Swagger-Editor ondersteunt OpenAPI 3.1 niet' + durationMustBeZeroOrGreaterExceptionMessage = 'Duur moet 0 of groter zijn, maar kreeg: {0}s' + viewsPathDoesNotExistExceptionMessage = 'Het pad voor views bestaat niet: {0}' + discriminatorIncompatibleWithAllOfExceptionMessage = "De parameter 'Discriminator' is niet compatibel met 'allOf'." + noNameForWebSocketSendMessageExceptionMessage = 'Geen naam opgegeven voor een WebSocket om een bericht naar te sturen.' + hashtableMiddlewareNoLogicExceptionMessage = 'Een opgegeven Hashtable Middleware heeft geen logica gedefinieerd.' + openApiInfoMessage = 'OpenAPI Info:' + invalidSchemeForAuthValidatorExceptionMessage = "Het opgegeven '{0}' schema voor de '{1}' authenticatievalidator vereist een geldige ScriptBlock." + sseFailedToBroadcastExceptionMessage = 'SSE kon niet uitzenden vanwege het gedefinieerde SSE-uitzendniveau voor {0}: {1}' + adModuleWindowsOnlyExceptionMessage = 'Active Directory-module alleen beschikbaar op Windows OS.' + requestLoggingAlreadyEnabledExceptionMessage = 'Verzoeklogboekregistratie is al ingeschakeld.' + invalidAccessControlMaxAgeDurationExceptionMessage = 'Ongeldige Access-Control-Max-Age duur opgegeven: {0}. Moet groter zijn dan 0.' + openApiDefinitionAlreadyExistsExceptionMessage = 'OpenAPI-definitie met de naam {0} bestaat al.' + renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag kan niet worden gebruikt binnen een Select-PodeOADefinition 'ScriptBlock'." + taskProcessDoesNotExistExceptionMessage = "Taakproces '{0}' bestaat niet." + scheduleProcessDoesNotExistExceptionMessage = "Schema-proces '{0}' bestaat niet." + definitionTagChangeNotAllowedExceptionMessage = 'Definitietag voor een route kan niet worden gewijzigd.' + getRequestBodyNotAllowedExceptionMessage = '{0}-operaties kunnen geen Request Body hebben.' + fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "De functie '{0}' accepteert geen array als pipeline-invoer." + unsupportedStreamCompressionEncodingExceptionMessage = 'Niet-ondersteunde streamcompressie-encodering: {0}' +} \ No newline at end of file diff --git a/src/Locales/pl/Pode.psd1 b/src/Locales/pl/Pode.psd1 new file mode 100644 index 000000000..afbcb3dc2 --- /dev/null +++ b/src/Locales/pl/Pode.psd1 @@ -0,0 +1,294 @@ +@{ + schemaValidationRequiresPowerShell610ExceptionMessage = 'Walidacja schematu wymaga wersji PowerShell 6.1.0 lub nowszej.' + customAccessPathOrScriptBlockRequiredExceptionMessage = 'Ścieżka lub ScriptBlock są wymagane do pozyskiwania wartości dostępu niestandardowego.' + operationIdMustBeUniqueForArrayExceptionMessage = 'OperationID: {0} musi być unikalny i nie może być zastosowany do tablicy.' + endpointNotDefinedForRedirectingExceptionMessage = "Nie zdefiniowano punktu końcowego o nazwie '{0}' do przekierowania." + filesHaveChangedMessage = 'Następujące pliki zostały zmienione:' + iisAspnetcoreTokenMissingExceptionMessage = 'Brakujący IIS ASPNETCORE_TOKEN.' + minValueGreaterThanMaxExceptionMessage = 'Minimalna wartość dla {0} nie powinna być większa od maksymalnej wartości.' + noLogicPassedForRouteExceptionMessage = 'Brak logiki przekazanej dla trasy: {0}' + scriptPathDoesNotExistExceptionMessage = 'Ścieżka skryptu nie istnieje: {0}' + mutexAlreadyExistsExceptionMessage = "Muteks o nazwie '{0}' już istnieje." + listeningOnEndpointsMessage = 'Nasłuchiwanie na następujących {0} punktach końcowych [{1} wątków]:' + unsupportedFunctionInServerlessContextExceptionMessage = 'Funkcja {0} nie jest obsługiwana w kontekście bezserwerowym.' + expectedNoJwtSignatureSuppliedExceptionMessage = 'Oczekiwano, że nie zostanie dostarczony żaden podpis JWT.' + secretAlreadyMountedExceptionMessage = "Tajemnica o nazwie '{0}' została już zamontowana." + failedToAcquireLockExceptionMessage = 'Nie udało się uzyskać blokady na obiekcie.' + noPathSuppliedForStaticRouteExceptionMessage = '[{0}]: Brak dostarczonej ścieżki dla trasy statycznej.' + invalidHostnameSuppliedExceptionMessage = 'Podano nieprawidłową nazwę hosta: {0}' + authMethodAlreadyDefinedExceptionMessage = 'Metoda uwierzytelniania już zdefiniowana: {0}' + csrfCookieRequiresSecretExceptionMessage = "Podczas używania ciasteczek do CSRF, wymagany jest Sekret. Możesz dostarczyć Sekret lub ustawić globalny sekret dla ciasteczek - (Set-PodeCookieSecret '' -Global)" + nonEmptyScriptBlockRequiredForPageRouteExceptionMessage = 'Aby utworzyć trasę strony, wymagany jest niepusty ScriptBlock.' + noPropertiesMutuallyExclusiveExceptionMessage = "Parametr 'NoProperties' jest wzajemnie wykluczający się z 'Properties', 'MinProperties' i 'MaxProperties'." + incompatiblePodeDllExceptionMessage = 'Istnieje niekompatybilna wersja Pode.DLL {0}. Wymagana wersja {1}. Otwórz nową sesję Powershell/pwsh i spróbuj ponownie.' + accessMethodDoesNotExistExceptionMessage = 'Metoda dostępu nie istnieje: {0}.' + scheduleAlreadyDefinedExceptionMessage = '[Harmonogram] {0}: Harmonogram już zdefiniowany.' + secondsValueCannotBeZeroOrLessExceptionMessage = 'Wartość sekund nie może być 0 lub mniejsza dla {0}' + pathToLoadNotFoundExceptionMessage = 'Ścieżka do załadowania {0} nie znaleziona: {1}' + failedToImportModuleExceptionMessage = 'Nie udało się zaimportować modułu: {0}' + endpointNotExistExceptionMessage = "Punkt końcowy z protokołem '{0}' i adresem '{1}' lub adresem lokalnym '{2}' nie istnieje." + terminatingMessage = 'Kończenie...' + noCommandsSuppliedToConvertToRoutesExceptionMessage = 'Nie dostarczono żadnych poleceń do konwersji na trasy.' + invalidTaskTypeExceptionMessage = 'Typ zadania jest nieprawidłowy, oczekiwano [System.Threading.Tasks.Task] lub [hashtable]' + alreadyConnectedToWebSocketExceptionMessage = "Już połączono z WebSocket o nazwie '{0}'" + crlfMessageEndCheckOnlySupportedOnTcpEndpointsExceptionMessage = 'Sprawdzanie końca wiadomości CRLF jest obsługiwane tylko na punktach końcowych TCP.' + testPodeOAComponentSchemaNeedToBeEnabledExceptionMessage = "'Test-PodeOAComponentSchema' musi być włączony przy użyciu 'Enable-PodeOpenApi -EnableSchemaValidation'" + adModuleNotInstalledExceptionMessage = 'Moduł Active Directory nie jest zainstalowany.' + cronExpressionInvalidExceptionMessage = 'Wyrażenie Cron powinno składać się tylko z 5 części: {0}' + noSessionToSetOnResponseExceptionMessage = 'Brak dostępnej sesji do ustawienia odpowiedzi.' + valueOutOfRangeExceptionMessage = "Wartość '{0}' dla {1} jest nieprawidłowa, powinna być pomiędzy {2} a {3}" + loggingMethodAlreadyDefinedExceptionMessage = 'Metoda logowania już zdefiniowana: {0}' + noSecretForHmac256ExceptionMessage = 'Nie podano tajemnicy dla haszowania HMAC256.' + eolPowerShellWarningMessage = '[OSTRZEŻENIE] Pode {0} nie był testowany na PowerShell {1}, ponieważ jest to wersja EOL.' + runspacePoolFailedToLoadExceptionMessage = '{0} Nie udało się załadować RunspacePool.' + noEventRegisteredExceptionMessage = 'Brak zarejestrowanego wydarzenia {0}: {1}' + scheduleCannotHaveNegativeLimitExceptionMessage = '[Harmonogram] {0}: Nie może mieć ujemnego limitu.' + openApiRequestStyleInvalidForParameterExceptionMessage = 'Styl żądania OpenApi nie może być {0} dla parametru {1}.' + openApiDocumentNotCompliantExceptionMessage = 'Dokument OpenAPI nie jest zgodny.' + taskDoesNotExistExceptionMessage = "Zadanie '{0}' nie istnieje." + scopedVariableNotFoundExceptionMessage = 'Nie znaleziono zmiennej zakresu: {0}' + sessionsRequiredForCsrfExceptionMessage = 'Sesje są wymagane do używania CSRF, chyba że chcesz używać ciasteczek.' + nonEmptyScriptBlockRequiredForLoggingMethodExceptionMessage = 'Metoda rejestrowania wymaga niepustego ScriptBlock.' + credentialsPassedWildcardForHeadersLiteralExceptionMessage = 'Gdy przekazywane są dane uwierzytelniające, symbol wieloznaczny * dla nagłówków będzie traktowany jako dosłowny ciąg znaków, a nie symbol wieloznaczny.' + podeNotInitializedExceptionMessage = 'Pode nie został zainicjowany.' + multipleEndpointsForGuiMessage = 'Zdefiniowano wiele punktów końcowych, tylko pierwszy będzie używany dla GUI.' + operationIdMustBeUniqueExceptionMessage = 'OperationID: {0} musi być unikalny.' + invalidJsonJwtExceptionMessage = 'Nieprawidłowa wartość JSON znaleziona w JWT' + noAlgorithmInJwtHeaderExceptionMessage = 'Brak dostarczonego algorytmu w nagłówku JWT.' + openApiVersionPropertyMandatoryExceptionMessage = 'Właściwość wersji OpenApi jest obowiązkowa.' + limitValueCannotBeZeroOrLessExceptionMessage = 'Wartość limitu nie może być 0 lub mniejsza dla {0}' + timerDoesNotExistExceptionMessage = "Timer '{0}' nie istnieje." + openApiGenerationDocumentErrorMessage = 'Błąd generowania dokumentu OpenAPI:' + routeAlreadyContainsCustomAccessExceptionMessage = "Trasa '[{0}] {1}' już zawiera dostęp niestandardowy z nazwą '{2}'" + maximumConcurrentWebSocketThreadsLessThanMinimumExceptionMessage = 'Maksymalna liczba jednoczesnych wątków WebSocket nie może być mniejsza niż minimum {0}, ale otrzymano: {1}' + middlewareAlreadyDefinedExceptionMessage = '[Middleware] {0}: Middleware już zdefiniowany.' + invalidAtomCharacterExceptionMessage = 'Nieprawidłowy znak atomu: {0}' + invalidCronAtomFormatExceptionMessage = 'Znaleziono nieprawidłowy format atomu cron: {0}' + cacheStorageNotFoundForRetrieveExceptionMessage = "Nie znaleziono magazynu pamięci podręcznej o nazwie '{0}' podczas próby pobrania elementu z pamięci podręcznej '{1}'." + headerMustHaveNameInEncodingContextExceptionMessage = 'Nagłówek musi mieć nazwę, gdy jest używany w kontekście kodowania.' + moduleDoesNotContainFunctionExceptionMessage = 'Moduł {0} nie zawiera funkcji {1} do konwersji na trasę.' + pathToIconForGuiDoesNotExistExceptionMessage = 'Ścieżka do ikony dla GUI nie istnieje: {0}' + noTitleSuppliedForPageExceptionMessage = 'Nie dostarczono tytułu dla strony {0}.' + certificateSuppliedForNonHttpsWssEndpointExceptionMessage = 'Certyfikat dostarczony dla punktu końcowego innego niż HTTPS/WSS.' + cannotLockNullObjectExceptionMessage = 'Nie można zablokować pustego obiektu.' + showPodeGuiOnlyAvailableOnWindowsExceptionMessage = 'Show-PodeGui jest obecnie dostępne tylko dla Windows PowerShell i PowerShell 7+ w Windows.' + unlockSecretButNoScriptBlockExceptionMessage = 'Podano tajemnicę odblokowania dla niestandardowego typu skarbca, ale nie podano ScriptBlock odblokowania.' + invalidIpAddressExceptionMessage = 'Podany adres IP jest nieprawidłowy: {0}' + maxDaysInvalidExceptionMessage = 'MaxDays musi wynosić 0 lub więcej, ale otrzymano: {0}' + noRemoveScriptBlockForVaultExceptionMessage = "Nie podano ScriptBlock dla usuwania tajemnic ze skarbca '{0}'" + noSecretExpectedForNoSignatureExceptionMessage = 'Nie oczekiwano podania tajemnicy dla braku podpisu.' + noCertificateFoundExceptionMessage = "Nie znaleziono certyfikatu w {0}{1} dla '{2}'" + minValueInvalidExceptionMessage = "Minimalna wartość '{0}' dla {1} jest nieprawidłowa, powinna być większa lub równa {2}" + accessRequiresAuthenticationOnRoutesExceptionMessage = 'Dostęp wymaga uwierzytelnienia na trasach.' + noSecretForHmac384ExceptionMessage = 'Nie podano tajemnicy dla haszowania HMAC384.' + windowsLocalAuthSupportIsForWindowsOnlyExceptionMessage = 'Wsparcie lokalnego uwierzytelniania Windows jest tylko dla Windows.' + definitionTagNotDefinedExceptionMessage = 'Etykieta definicji {0} nie jest zdefiniowana.' + noComponentInDefinitionExceptionMessage = 'Brak komponentu typu {0} o nazwie {1} dostępnego w definicji {2}.' + noSmtpHandlersDefinedExceptionMessage = 'Nie zdefiniowano żadnych obsługujących SMTP.' + sessionMiddlewareAlreadyInitializedExceptionMessage = 'Middleware sesji został już zainicjowany.' + reusableComponentPathItemsNotAvailableInOpenApi30ExceptionMessage = "Funkcja wielokrotnego użytku 'pathItems' nie jest dostępna w OpenAPI v3.0." + wildcardHeadersIncompatibleWithAutoHeadersExceptionMessage = 'Symbol wieloznaczny * dla nagłówków jest niezgodny z przełącznikiem AutoHeaders.' + noDataForFileUploadedExceptionMessage = "Brak danych dla pliku '{0}' przesłanego w żądaniu." + sseOnlyConfiguredOnEventStreamAcceptHeaderExceptionMessage = 'SSE można skonfigurować tylko na żądaniach z wartością nagłówka Accept równą text/event-stream.' + noSessionAvailableToSaveExceptionMessage = 'Brak dostępnej sesji do zapisania.' + pathParameterRequiresRequiredSwitchExceptionMessage = "Jeśli lokalizacja parametru to 'Path', przełącznik 'Required' jest obowiązkowy." + noOpenApiUrlSuppliedExceptionMessage = 'Nie dostarczono adresu URL OpenAPI dla {0}.' + maximumConcurrentSchedulesInvalidExceptionMessage = 'Maksymalna liczba równoczesnych harmonogramów musi wynosić >=1, ale otrzymano: {0}' + snapinsSupportedOnWindowsPowershellOnlyExceptionMessage = 'Snapiny są obsługiwane tylko w Windows PowerShell.' + eventViewerLoggingSupportedOnWindowsOnlyExceptionMessage = 'Rejestrowanie w Podglądzie zdarzeń jest obsługiwane tylko w systemie Windows.' + parametersMutuallyExclusiveExceptionMessage = "Parametry '{0}' i '{1}' są wzajemnie wykluczające się." + pathItemsFeatureNotSupportedInOpenApi30ExceptionMessage = 'Funkcja PathItems nie jest obsługiwana w OpenAPI v3.0.x' + openApiParameterRequiresNameExceptionMessage = 'Parametr OpenApi wymaga podania nazwy.' + maximumConcurrentTasksLessThanMinimumExceptionMessage = 'Maksymalna liczba jednoczesnych zadań nie może być mniejsza niż minimum {0}, ale otrzymano: {1}' + noSemaphoreFoundExceptionMessage = "Nie znaleziono semaforu o nazwie '{0}'" + singleValueForIntervalExceptionMessage = 'Możesz podać tylko jedną wartość {0} podczas korzystania z interwałów.' + jwtNotYetValidExceptionMessage = 'JWT jeszcze nie jest ważny.' + verbAlreadyDefinedForUrlExceptionMessage = '[Czasownik] {0}: Już zdefiniowane dla {1}' + noSecretNamedMountedExceptionMessage = "Nie zamontowano tajemnicy o nazwie '{0}'." + moduleOrVersionNotFoundExceptionMessage = 'Nie znaleziono modułu lub wersji na {0}: {1}@{2}' + noScriptBlockSuppliedExceptionMessage = 'Nie podano ScriptBlock.' + noSecretVaultRegisteredExceptionMessage = "Nie zarejestrowano Skarbca Tajemnic o nazwie '{0}'." + nameRequiredForEndpointIfRedirectToSuppliedExceptionMessage = 'Nazwa jest wymagana dla punktu końcowego, jeśli podano parametr RedirectTo.' + openApiLicenseObjectRequiresNameExceptionMessage = "Obiekt OpenAPI 'license' wymaga właściwości 'name'. Użyj parametru -LicenseName." + sourcePathDoesNotExistForStaticRouteExceptionMessage = '{0}: Dostarczona ścieżka źródłowa dla trasy statycznej nie istnieje: {1}' + noNameForWebSocketDisconnectExceptionMessage = 'Nie podano nazwy dla rozłączenia WebSocket.' + certificateExpiredExceptionMessage = "Certyfikat '{0}' wygasł: {1}" + secretVaultUnlockExpiryDateInPastExceptionMessage = 'Data wygaśnięcia odblokowania Skarbca tajemnic jest w przeszłości (UTC): {0}' + invalidWebExceptionTypeExceptionMessage = 'Wyjątek jest nieprawidłowego typu, powinien być WebException lub HttpRequestException, ale otrzymano: {0}' + invalidSecretValueTypeExceptionMessage = 'Wartość tajemnicy jest nieprawidłowego typu. Oczekiwane typy: String, SecureString, HashTable, Byte[] lub PSCredential. Ale otrzymano: {0}' + explicitTlsModeOnlySupportedOnSmtpsTcpsEndpointsExceptionMessage = 'Tryb TLS Explicity jest obsługiwany tylko na punktach końcowych SMTPS i TCPS.' + discriminatorMappingRequiresDiscriminatorPropertyExceptionMessage = "Parametr 'DiscriminatorMapping' może być używany tylko wtedy, gdy jest obecna właściwość 'DiscriminatorProperty'." + scriptErrorExceptionMessage = "Błąd '{0}' w skrypcie {1} {2} (linia {3}) znak {4} podczas wykonywania {5} na {6} obiekt '{7}' Klasa: {8} Klasa bazowa: {9}" + cannotSupplyIntervalForQuarterExceptionMessage = 'Nie można dostarczyć wartości interwału dla każdego kwartału.' + scheduleEndTimeMustBeInFutureExceptionMessage = '[Harmonogram] {0}: Wartość EndTime musi być w przyszłości.' + invalidJwtSignatureSuppliedExceptionMessage = 'Dostarczono nieprawidłowy podpis JWT.' + noSetScriptBlockForVaultExceptionMessage = "Nie podano ScriptBlock dla aktualizacji/tworzenia tajemnic w skarbcu '{0}'" + accessMethodNotExistForMergingExceptionMessage = 'Metoda dostępu nie istnieje do scalania: {0}' + defaultAuthNotInListExceptionMessage = "Domyślne uwierzytelnianie '{0}' nie znajduje się na dostarczonej liście uwierzytelniania." + parameterHasNoNameExceptionMessage = "Parametr nie ma nazwy. Proszę nadać tej części nazwę za pomocą parametru 'Name'." + methodPathAlreadyDefinedForUrlExceptionMessage = '[{0}] {1}: Już zdefiniowane dla {2}' + fileWatcherAlreadyDefinedExceptionMessage = "Obserwator plików o nazwie '{0}' został już zdefiniowany." + noServiceHandlersDefinedExceptionMessage = 'Nie zdefiniowano żadnych obsługujących usług.' + secretRequiredForCustomSessionStorageExceptionMessage = 'Podczas korzystania z niestandardowego przechowywania sesji wymagany jest sekret.' + secretManagementModuleNotInstalledExceptionMessage = 'Moduł Microsoft.PowerShell.SecretManagement nie jest zainstalowany.' + noPathSuppliedForRouteExceptionMessage = 'Nie podano ścieżki dla trasy.' + validationOfAnyOfSchemaNotSupportedExceptionMessage = "Walidacja schematu, który zawiera 'anyof', nie jest obsługiwana." + iisAuthSupportIsForWindowsOnlyExceptionMessage = 'Wsparcie uwierzytelniania IIS jest tylko dla Windows.' + oauth2InnerSchemeInvalidExceptionMessage = 'OAuth2 InnerScheme może być tylko jednym z dwóch: Basic lub Form, ale otrzymano: {0}' + noRoutePathSuppliedForPageExceptionMessage = 'Nie dostarczono ścieżki trasy dla strony {0}.' + cacheStorageNotFoundForExistsExceptionMessage = "Nie znaleziono magazynu pamięci podręcznej o nazwie '{0}' podczas próby sprawdzenia, czy element w pamięci podręcznej '{1}' istnieje." + handlerAlreadyDefinedExceptionMessage = '[{0}] {1}: Handler już zdefiniowany.' + sessionsNotConfiguredExceptionMessage = 'Sesje nie zostały skonfigurowane.' + propertiesTypeObjectAssociationExceptionMessage = 'Tylko właściwości typu Object mogą być powiązane z {0}.' + sessionsRequiredForSessionPersistentAuthExceptionMessage = 'Sesje są wymagane do używania trwałego uwierzytelniania sesji.' + invalidPathWildcardOrDirectoryExceptionMessage = 'Podana ścieżka nie może być symbolem wieloznacznym ani katalogiem: {0}' + accessMethodAlreadyDefinedExceptionMessage = 'Metoda dostępu już zdefiniowana: {0}' + parametersValueOrExternalValueMandatoryExceptionMessage = "Parametry 'Value' lub 'ExternalValue' są obowiązkowe." + maximumConcurrentTasksInvalidExceptionMessage = 'Maksymalna liczba jednoczesnych zadań musi wynosić >=1, ale otrzymano: {0}' + cannotCreatePropertyWithoutTypeExceptionMessage = 'Nie można utworzyć właściwości, ponieważ nie zdefiniowano typu.' + authMethodNotExistForMergingExceptionMessage = 'Metoda uwierzytelniania nie istnieje dla scalania: {0}' + maxValueInvalidExceptionMessage = "Maksymalna wartość '{0}' dla {1} jest nieprawidłowa, powinna być mniejsza lub równa {2}" + endpointAlreadyDefinedExceptionMessage = "Punkt końcowy o nazwie '{0}' został już zdefiniowany." + eventAlreadyRegisteredExceptionMessage = 'Wydarzenie {0} już zarejestrowane: {1}' + parameterNotSuppliedInRequestExceptionMessage = "Parametr o nazwie '{0}' nie został dostarczony w żądaniu lub nie ma dostępnych danych." + cacheStorageNotFoundForSetExceptionMessage = "Nie znaleziono magazynu pamięci podręcznej o nazwie '{0}' podczas próby ustawienia elementu w pamięci podręcznej '{1}'." + methodPathAlreadyDefinedExceptionMessage = '[{0}] {1}: Już zdefiniowane.' + errorLoggingAlreadyEnabledExceptionMessage = 'Rejestrowanie błędów jest już włączone.' + valueForUsingVariableNotFoundExceptionMessage = "Nie można znaleźć wartości dla '`$using:{0}'." + rapidPdfDoesNotSupportOpenApi31ExceptionMessage = 'Narzędzie do dokumentów RapidPdf nie obsługuje OpenAPI 3.1' + oauth2ClientSecretRequiredExceptionMessage = 'OAuth2 wymaga tajemnicy klienta, gdy nie używa się PKCE.' + invalidBase64JwtExceptionMessage = 'Nieprawidłowa wartość zakodowana w Base64 znaleziona w JWT' + noSessionToCalculateDataHashExceptionMessage = 'Brak dostępnej sesji do obliczenia skrótu danych.' + cacheStorageNotFoundForRemoveExceptionMessage = "Nie znaleziono magazynu pamięci podręcznej o nazwie '{0}' podczas próby usunięcia elementu z pamięci podręcznej '{1}'." + csrfMiddlewareNotInitializedExceptionMessage = 'Middleware CSRF nie został zainicjowany.' + infoTitleMandatoryMessage = 'info.title jest obowiązkowe.' + typeCanOnlyBeAssociatedWithObjectExceptionMessage = 'Typ {0} może być powiązany tylko z obiektem.' + userFileDoesNotExistExceptionMessage = 'Plik użytkownika nie istnieje: {0}' + routeParameterNeedsValidScriptblockExceptionMessage = 'Parametr trasy wymaga prawidłowego, niepustego ScriptBlock.' + nextTriggerCalculationErrorExceptionMessage = 'Wygląda na to, że coś poszło nie tak przy próbie obliczenia następnej daty i godziny wyzwalacza: {0}' + cannotLockValueTypeExceptionMessage = 'Nie można zablokować [ValueType].' + failedToCreateOpenSslCertExceptionMessage = 'Nie udało się utworzyć certyfikatu OpenSSL: {0}' + jwtExpiredExceptionMessage = 'JWT wygasł.' + openingGuiMessage = 'Otwieranie GUI.' + multiTypePropertiesRequireOpenApi31ExceptionMessage = 'Właściwości wielotypowe wymagają wersji OpenApi 3.1 lub wyższej.' + noNameForWebSocketRemoveExceptionMessage = 'Nie podano nazwy dla usunięcia WebSocket.' + maxSizeInvalidExceptionMessage = 'MaxSize musi wynosić 0 lub więcej, ale otrzymano: {0}' + iisShutdownMessage = '(Zamykanie IIS)' + cannotUnlockValueTypeExceptionMessage = 'Nie można odblokować [ValueType].' + noJwtSignatureForAlgorithmExceptionMessage = 'Nie dostarczono podpisu JWT dla {0}.' + maximumConcurrentWebSocketThreadsInvalidExceptionMessage = 'Maksymalna liczba jednoczesnych wątków WebSocket musi wynosić >=1, ale otrzymano: {0}' + acknowledgeMessageOnlySupportedOnSmtpTcpEndpointsExceptionMessage = 'Komunikat potwierdzenia jest obsługiwany tylko na punktach końcowych SMTP i TCP.' + failedToConnectToUrlExceptionMessage = 'Nie udało się połączyć z URL: {0}' + failedToAcquireMutexOwnershipExceptionMessage = 'Nie udało się przejąć własności muteksu. Nazwa muteksu: {0}' + sessionsRequiredForOAuth2WithPKCEExceptionMessage = 'Sesje są wymagane do używania OAuth2 z PKCE' + failedToConnectToWebSocketExceptionMessage = 'Nie udało się połączyć z WebSocket: {0}' + unsupportedObjectExceptionMessage = 'Obiekt nieobsługiwany' + failedToParseAddressExceptionMessage = "Nie udało się przeanalizować '{0}' jako poprawnego adresu IP/Host:Port" + mustBeRunningWithAdminPrivilegesExceptionMessage = 'Musisz mieć uprawnienia administratora, aby nasłuchiwać na adresach innych niż localhost.' + specificationMessage = 'Specyfikacja' + cacheStorageNotFoundForClearExceptionMessage = "Nie znaleziono magazynu pamięci podręcznej o nazwie '{0}' podczas próby wyczyszczenia pamięci podręcznej." + restartingServerMessage = 'Restartowanie serwera...' + cannotSupplyIntervalWhenEveryIsNoneExceptionMessage = "Nie można dostarczyć interwału, gdy parametr 'Every' jest ustawiony na None." + unsupportedJwtAlgorithmExceptionMessage = 'Algorytm JWT nie jest obecnie obsługiwany: {0}' + websocketsNotConfiguredForSignalMessagesExceptionMessage = 'WebSockets nie zostały skonfigurowane do wysyłania wiadomości sygnałowych.' + invalidLogicTypeInHashtableMiddlewareExceptionMessage = 'Dostarczone Middleware typu Hashtable ma nieprawidłowy typ logiki. Oczekiwano ScriptBlock, ale otrzymano: {0}' + maximumConcurrentSchedulesLessThanMinimumExceptionMessage = 'Maksymalna liczba równoczesnych harmonogramów nie może być mniejsza niż minimalna liczba {0}, ale otrzymano: {1}' + failedToAcquireSemaphoreOwnershipExceptionMessage = 'Nie udało się przejąć własności semaforu. Nazwa semaforu: {0}' + propertiesParameterWithoutNameExceptionMessage = 'Parametry Properties nie mogą być używane, jeśli właściwość nie ma nazwy.' + customSessionStorageMethodNotImplementedExceptionMessage = "Niestandardowe przechowywanie sesji nie implementuje wymaganego ''{0}()'' sposobu." + authenticationMethodDoesNotExistExceptionMessage = 'Metoda uwierzytelniania nie istnieje: {0}' + webhooksFeatureNotSupportedInOpenApi30ExceptionMessage = 'Funkcja Webhooks nie jest obsługiwana w OpenAPI v3.0.x' + invalidContentTypeForSchemaExceptionMessage = "Nieprawidłowy 'content-type' znaleziony w schemacie: {0}" + noUnlockScriptBlockForVaultExceptionMessage = "Nie podano ScriptBlock odblokowania dla odblokowania skarbca '{0}'" + definitionTagMessage = 'Definicja {0}:' + failedToOpenRunspacePoolExceptionMessage = 'Nie udało się otworzyć RunspacePool: {0}' + failedToCloseRunspacePoolExceptionMessage = 'Nie udało się zamknąć RunspacePool: {0}' + verbNoLogicPassedExceptionMessage = '[Czasownik] {0}: Nie przekazano logiki' + noMutexFoundExceptionMessage = "Nie znaleziono muteksu o nazwie '{0}'." + documentationMessage = 'Dokumentacja' + timerAlreadyDefinedExceptionMessage = '[Timer] {0}: Timer już zdefiniowany.' + invalidPortExceptionMessage = 'Port nie może być ujemny: {0}' + viewsFolderNameAlreadyExistsExceptionMessage = 'Nazwa folderu Widoków już istnieje: {0}' + noNameForWebSocketResetExceptionMessage = 'Nie podano nazwy dla resetowania WebSocket.' + mergeDefaultAuthNotInListExceptionMessage = "Uwierzytelnianie MergeDefault '{0}' nie znajduje się na dostarczonej liście uwierzytelniania." + descriptionRequiredExceptionMessage = 'Wymagany jest opis dla ścieżki:{0} Odpowiedź:{1}' + pageNameShouldBeAlphaNumericExceptionMessage = 'Nazwa strony powinna być poprawną wartością alfanumeryczną: {0}' + defaultValueNotBooleanOrEnumExceptionMessage = 'Wartość domyślna nie jest typu boolean i nie należy do enum.' + openApiComponentSchemaDoesNotExistExceptionMessage = 'Schemat komponentu OpenApi {0} nie istnieje.' + timerParameterMustBeGreaterThanZeroExceptionMessage = '[Timer] {0}: {1} musi być większy od 0.' + taskTimedOutExceptionMessage = 'Zadanie przekroczyło limit czasu po {0}ms.' + scheduleStartTimeAfterEndTimeExceptionMessage = "[Harmonogram] {0}: Nie może mieć 'StartTime' po 'EndTime'." + infoVersionMandatoryMessage = 'info.version jest obowiązkowe.' + cannotUnlockNullObjectExceptionMessage = 'Nie można odblokować pustego obiektu.' + nonEmptyScriptBlockRequiredForCustomAuthExceptionMessage = 'Dla niestandardowego schematu uwierzytelniania wymagany jest niepusty ScriptBlock.' + nonEmptyScriptBlockRequiredForAuthMethodExceptionMessage = 'Wymagany jest niepusty ScriptBlock dla metody uwierzytelniania.' + validationOfOneOfSchemaNotSupportedExceptionMessage = "Walidacja schematu, który zawiera 'oneof', nie jest obsługiwana." + routeParameterCannotBeNullExceptionMessage = "Parametr 'Route' nie może być pusty." + cacheStorageAlreadyExistsExceptionMessage = "Magazyn pamięci podręcznej o nazwie '{0}' już istnieje." + loggingMethodRequiresValidScriptBlockExceptionMessage = "Dostarczona metoda wyjściowa dla metody logowania '{0}' wymaga poprawnego ScriptBlock." + scopedVariableAlreadyDefinedExceptionMessage = 'Zmienna z zakresem już zdefiniowana: {0}' + oauth2RequiresAuthorizeUrlExceptionMessage = 'OAuth2 wymaga podania URL autoryzacji' + pathNotExistExceptionMessage = 'Ścieżka nie istnieje: {0}' + noDomainServerNameForWindowsAdAuthExceptionMessage = 'Nie podano nazwy serwera domeny dla uwierzytelniania Windows AD' + suppliedDateAfterScheduleEndTimeExceptionMessage = 'Podana data jest późniejsza niż czas zakończenia harmonogramu o {0}' + wildcardMethodsIncompatibleWithAutoMethodsExceptionMessage = 'Symbol wieloznaczny * dla metod jest niezgodny z przełącznikiem AutoMethods.' + cannotSupplyIntervalForYearExceptionMessage = 'Nie można dostarczyć wartości interwału dla każdego roku.' + missingComponentsMessage = 'Brakujące komponenty' + invalidStrictTransportSecurityDurationExceptionMessage = 'Nieprawidłowy czas trwania Strict-Transport-Security: {0}. Powinien być większy niż 0.' + noSecretForHmac512ExceptionMessage = 'Nie podano tajemnicy dla haszowania HMAC512.' + daysInMonthExceededExceptionMessage = '{0} ma tylko {1} dni, ale podano {2}.' + nonEmptyScriptBlockRequiredForCustomLoggingExceptionMessage = 'Metoda niestandardowego rejestrowania wymaga niepustego ScriptBlock.' + encodingAttributeOnlyAppliesToMultipartExceptionMessage = 'Atrybut kodowania dotyczy tylko ciał żądania typu multipart i application/x-www-form-urlencoded.' + suppliedDateBeforeScheduleStartTimeExceptionMessage = 'Podana data jest wcześniejsza niż czas rozpoczęcia harmonogramu o {0}' + unlockSecretRequiredExceptionMessage = "Właściwość 'UnlockSecret' jest wymagana przy używaniu Microsoft.PowerShell.SecretStore" + noLogicPassedForMethodRouteExceptionMessage = '[{0}] {1}: Brak logiki przekazanej.' + bodyParserAlreadyDefinedForContentTypeExceptionMessage = 'Parser treści dla typu zawartości {0} jest już zdefiniowany.' + invalidJwtSuppliedExceptionMessage = 'Dostarczono nieprawidłowy JWT.' + sessionsRequiredForFlashMessagesExceptionMessage = 'Sesje są wymagane do używania wiadomości Flash.' + semaphoreAlreadyExistsExceptionMessage = "Semafor o nazwie '{0}' już istnieje." + invalidJwtHeaderAlgorithmSuppliedExceptionMessage = 'Dostarczono nieprawidłowy algorytm nagłówka JWT.' + oauth2ProviderDoesNotSupportPasswordGrantTypeExceptionMessage = "Dostawca OAuth2 nie obsługuje typu 'password' wymaganego przez InnerScheme." + invalidAliasFoundExceptionMessage = 'Znaleziono nieprawidłowy alias {0}: {1}' + scheduleDoesNotExistExceptionMessage = "Harmonogram '{0}' nie istnieje." + accessMethodNotExistExceptionMessage = 'Metoda dostępu nie istnieje: {0}' + oauth2ProviderDoesNotSupportCodeResponseTypeExceptionMessage = "Dostawca OAuth2 nie obsługuje typu odpowiedzi 'code'." + untestedPowerShellVersionWarningMessage = '[OSTRZEŻENIE] Pode {0} nie był testowany na PowerShell {1}, ponieważ nie był dostępny, gdy Pode został wydany.' + secretVaultAlreadyRegisteredAutoImportExceptionMessage = "Skarbiec z nazwą '{0}' został już zarejestrowany podczas automatycznego importowania skarbców." + schemeRequiresValidScriptBlockExceptionMessage = "Dostarczony schemat dla walidatora uwierzytelniania '{0}' wymaga ważnego ScriptBlock." + serverLoopingMessage = 'Pętla serwera co {0} sekund' + certificateThumbprintsNameSupportedOnWindowsExceptionMessage = 'Odciski palców/nazwa certyfikatu są obsługiwane tylko w systemie Windows.' + sseConnectionNameRequiredExceptionMessage = "Wymagana jest nazwa połączenia SSE, z -Name lub `$WebEvent.Sse.Name" + invalidMiddlewareTypeExceptionMessage = 'Jeden z dostarczonych Middleware jest nieprawidłowego typu. Oczekiwano ScriptBlock lub Hashtable, ale otrzymano: {0}' + noSecretForJwtSignatureExceptionMessage = 'Nie podano tajemnicy dla podpisu JWT.' + modulePathDoesNotExistExceptionMessage = 'Ścieżka modułu nie istnieje: {0}' + taskAlreadyDefinedExceptionMessage = '[Zadanie] {0}: Zadanie już zdefiniowane.' + verbAlreadyDefinedExceptionMessage = '[Czasownik] {0}: Już zdefiniowane' + clientCertificatesOnlySupportedOnHttpsEndpointsExceptionMessage = 'Certyfikaty klienta są obsługiwane tylko na punktach końcowych HTTPS.' + endpointNameNotExistExceptionMessage = "Punkt końcowy o nazwie '{0}' nie istnieje." + middlewareNoLogicSuppliedExceptionMessage = '[Middleware]: Nie dostarczono logiki w ScriptBlock.' + scriptBlockRequiredForMergingUsersExceptionMessage = 'Wymagany jest ScriptBlock do scalania wielu uwierzytelnionych użytkowników w jeden obiekt, gdy opcja Valid to All.' + secretVaultAlreadyRegisteredExceptionMessage = "Skarbiec tajemnic o nazwie '{0}' został już zarejestrowany{1}." + deprecatedTitleVersionDescriptionWarningMessage = "OSTRZEŻENIE: Tytuł, Wersja i Opis w 'Enable-PodeOpenApi' są przestarzałe. Proszę użyć 'Add-PodeOAInfo' zamiast tego." + undefinedOpenApiReferencesMessage = 'Niezdefiniowane odwołania OpenAPI:' + doneMessage = 'Gotowe' + swaggerEditorDoesNotSupportOpenApi31ExceptionMessage = 'Ta wersja Swagger-Editor nie obsługuje OpenAPI 3.1' + durationMustBeZeroOrGreaterExceptionMessage = 'Czas trwania musi wynosić 0 lub więcej, ale otrzymano: {0}s' + viewsPathDoesNotExistExceptionMessage = 'Ścieżka do Widoków nie istnieje: {0}' + discriminatorIncompatibleWithAllOfExceptionMessage = "Parametr 'Discriminator' jest niezgodny z 'allOf'." + noNameForWebSocketSendMessageExceptionMessage = 'Nie podano nazwy dla wysłania wiadomości do WebSocket.' + hashtableMiddlewareNoLogicExceptionMessage = 'Dostarczone Middleware typu Hashtable nie ma zdefiniowanej logiki.' + openApiInfoMessage = 'Informacje OpenAPI:' + invalidSchemeForAuthValidatorExceptionMessage = "Dostarczony schemat '{0}' dla walidatora uwierzytelniania '{1}' wymaga ważnego ScriptBlock." + sseFailedToBroadcastExceptionMessage = 'SSE nie udało się przesłać z powodu zdefiniowanego poziomu przesyłania SSE dla {0}: {1}' + adModuleWindowsOnlyExceptionMessage = 'Moduł Active Directory jest dostępny tylko w systemie Windows.' + requestLoggingAlreadyEnabledExceptionMessage = 'Rejestrowanie żądań jest już włączone.' + invalidAccessControlMaxAgeDurationExceptionMessage = 'Podano nieprawidłowy czas trwania Access-Control-Max-Age: {0}. Powinien być większy niż 0.' + openApiDefinitionAlreadyExistsExceptionMessage = 'Definicja OpenAPI o nazwie {0} już istnieje.' + renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag nie może być używany wewnątrz 'ScriptBlock' Select-PodeOADefinition." + taskProcessDoesNotExistExceptionMessage = "Proces zadania '{0}' nie istnieje." + scheduleProcessDoesNotExistExceptionMessage = "Proces harmonogramu '{0}' nie istnieje." + definitionTagChangeNotAllowedExceptionMessage = 'Tag definicji dla Route nie może zostać zmieniony.' + getRequestBodyNotAllowedExceptionMessage = 'Operacje {0} nie mogą mieć treści żądania.' + fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "Funkcja '{0}' nie akceptuje tablicy jako wejścia potoku." + unsupportedStreamCompressionEncodingExceptionMessage = 'Kodowanie kompresji strumienia nie jest obsługiwane: {0}' +} \ No newline at end of file diff --git a/src/Locales/pt/Pode.psd1 b/src/Locales/pt/Pode.psd1 new file mode 100644 index 000000000..af6d8731b --- /dev/null +++ b/src/Locales/pt/Pode.psd1 @@ -0,0 +1,294 @@ +@{ + schemaValidationRequiresPowerShell610ExceptionMessage = 'A validação do esquema requer a versão 6.1.0 ou superior do PowerShell.' + customAccessPathOrScriptBlockRequiredExceptionMessage = 'É necessário um Caminho ou ScriptBlock para obter os valores de acesso personalizados.' + operationIdMustBeUniqueForArrayExceptionMessage = 'OperationID: {0} deve ser único e não pode ser aplicado a uma matriz.' + endpointNotDefinedForRedirectingExceptionMessage = "Não foi definido um ponto de extremidade chamado '{0}' para redirecionamento." + filesHaveChangedMessage = 'Os seguintes arquivos foram alterados:' + iisAspnetcoreTokenMissingExceptionMessage = 'IIS ASPNETCORE_TOKEN está ausente.' + minValueGreaterThanMaxExceptionMessage = 'O valor mínimo para {0} não deve ser maior que o valor máximo.' + noLogicPassedForRouteExceptionMessage = 'Nenhuma lógica passada para a Rota: {0}' + scriptPathDoesNotExistExceptionMessage = 'O caminho do script não existe: {0}' + mutexAlreadyExistsExceptionMessage = 'Já existe um mutex com o seguinte nome: {0}' + listeningOnEndpointsMessage = 'Ouvindo nos seguintes {0} endpoint(s) [{1} thread(s)]:' + unsupportedFunctionInServerlessContextExceptionMessage = 'A função {0} não é suportada em um contexto serverless.' + expectedNoJwtSignatureSuppliedExceptionMessage = 'Esperava-se que nenhuma assinatura JWT fosse fornecida.' + secretAlreadyMountedExceptionMessage = "Um Segredo com o nome '{0}' já foi montado." + failedToAcquireLockExceptionMessage = 'Falha ao adquirir um bloqueio no objeto.' + noPathSuppliedForStaticRouteExceptionMessage = '[{0}]: Nenhum caminho fornecido para a Rota Estática.' + invalidHostnameSuppliedExceptionMessage = 'Nome de host fornecido inválido: {0}' + authMethodAlreadyDefinedExceptionMessage = 'Método de autenticação já definido: {0}' + csrfCookieRequiresSecretExceptionMessage = "Ao usar cookies para CSRF, é necessário um Segredo. Você pode fornecer um Segredo ou definir o segredo global do Cookie - (Set-PodeCookieSecret '' -Global)" + nonEmptyScriptBlockRequiredForPageRouteExceptionMessage = 'Um ScriptBlock não vazio é necessário para criar uma Rota de Página.' + noPropertiesMutuallyExclusiveExceptionMessage = "O parâmetro 'NoProperties' é mutuamente exclusivo com 'Properties', 'MinProperties' e 'MaxProperties'." + incompatiblePodeDllExceptionMessage = 'Uma versão incompatível existente do Pode.DLL {0} está carregada. É necessária a versão {1}. Abra uma nova sessão do Powershell/pwsh e tente novamente.' + accessMethodDoesNotExistExceptionMessage = 'O método de acesso não existe: {0}.' + scheduleAlreadyDefinedExceptionMessage = '[Cronograma] {0}: Cronograma já definida.' + secondsValueCannotBeZeroOrLessExceptionMessage = 'O valor dos segundos não pode ser 0 ou inferior para {0}' + pathToLoadNotFoundExceptionMessage = 'Caminho para carregar {0} não encontrado: {1}' + failedToImportModuleExceptionMessage = 'Falha ao importar módulo: {0}' + endpointNotExistExceptionMessage = "O ponto de extremidade com o protocolo '{0}' e endereço '{1}' ou endereço local '{2}' não existe." + terminatingMessage = 'Terminando...' + noCommandsSuppliedToConvertToRoutesExceptionMessage = 'Nenhum comando fornecido para converter em Rotas.' + invalidTaskTypeExceptionMessage = 'O tipo de tarefa é inválido, esperado [System.Threading.Tasks.Task] ou [hashtable].' + alreadyConnectedToWebSocketExceptionMessage = "Já conectado ao websocket com o nome '{0}'" + crlfMessageEndCheckOnlySupportedOnTcpEndpointsExceptionMessage = 'A verificação de fim de mensagem CRLF é suportada apenas em endpoints TCP.' + testPodeOAComponentSchemaNeedToBeEnabledExceptionMessage = "'Test-PodeOAComponentSchema' precisa ser habilitado usando 'Enable-PodeOpenApi -EnableSchemaValidation'" + adModuleNotInstalledExceptionMessage = 'O módulo Active Directory não está instalado.' + cronExpressionInvalidExceptionMessage = 'A expressão Cron deve consistir apenas em 5 partes: {0}' + noSessionToSetOnResponseExceptionMessage = 'Não há sessão disponível para definir na resposta.' + valueOutOfRangeExceptionMessage = "O valor '{0}' para {1} é inválido, deve estar entre {2} e {3}" + loggingMethodAlreadyDefinedExceptionMessage = 'Método de registro já definido: {0}' + noSecretForHmac256ExceptionMessage = 'Nenhum segredo fornecido para o hash HMAC256.' + eolPowerShellWarningMessage = '[AVISO] Pode {0} não foi testado no PowerShell {1}, pois está em EOL.' + runspacePoolFailedToLoadExceptionMessage = '{0} Falha ao carregar RunspacePool.' + noEventRegisteredExceptionMessage = 'Nenhum evento {0} registrado: {1}' + scheduleCannotHaveNegativeLimitExceptionMessage = '[Cronograma] {0}: Não pode ter um limite negativo.' + openApiRequestStyleInvalidForParameterExceptionMessage = 'O estilo da solicitação OpenApi não pode ser {0} para um parâmetro {1}.' + openApiDocumentNotCompliantExceptionMessage = 'O documento OpenAPI não está em conformidade.' + taskDoesNotExistExceptionMessage = "A tarefa '{0}' não existe." + scopedVariableNotFoundExceptionMessage = 'Variável de escopo não encontrada: {0}' + sessionsRequiredForCsrfExceptionMessage = 'Sessões são necessárias para usar CSRF, a menos que você queira usar cookies.' + nonEmptyScriptBlockRequiredForLoggingMethodExceptionMessage = 'Um ScriptBlock não vazio é necessário para o método de registro.' + credentialsPassedWildcardForHeadersLiteralExceptionMessage = 'Quando as Credenciais são passadas, o caractere curinga * para os Cabeçalhos será interpretado como uma string literal e não como um caractere curinga.' + podeNotInitializedExceptionMessage = 'Pode não foi inicializado.' + multipleEndpointsForGuiMessage = 'Múltiplos endpoints definidos, apenas o primeiro será usado para a GUI.' + operationIdMustBeUniqueExceptionMessage = 'OperationID: {0} deve ser único.' + invalidJsonJwtExceptionMessage = 'Valor JSON inválido encontrado no JWT' + noAlgorithmInJwtHeaderExceptionMessage = 'Nenhum algoritmo fornecido no Cabeçalho JWT.' + openApiVersionPropertyMandatoryExceptionMessage = 'A propriedade da versão do OpenApi é obrigatória.' + limitValueCannotBeZeroOrLessExceptionMessage = 'O valor limite não pode ser 0 ou inferior para {0}' + timerDoesNotExistExceptionMessage = "O temporizador '{0}' não existe." + openApiGenerationDocumentErrorMessage = 'Erro no documento de geração do OpenAPI:' + routeAlreadyContainsCustomAccessExceptionMessage = "A rota '[{0}] {1}' já contém Acesso Personalizado com o nome '{2}'" + maximumConcurrentWebSocketThreadsLessThanMinimumExceptionMessage = 'O número máximo de threads concorrentes do WebSocket não pode ser menor que o mínimo de {0}, mas foi obtido: {1}' + middlewareAlreadyDefinedExceptionMessage = '[Middleware] {0}: Middleware já definido.' + invalidAtomCharacterExceptionMessage = 'Caractere atômico inválido: {0}' + invalidCronAtomFormatExceptionMessage = 'Formato de átomo cron inválido encontrado: {0}' + cacheStorageNotFoundForRetrieveExceptionMessage = "Armazenamento em cache com o nome '{0}' não encontrado ao tentar recuperar o item em cache '{1}'." + headerMustHaveNameInEncodingContextExceptionMessage = 'O cabeçalho deve ter um nome quando usado em um contexto de codificação.' + moduleDoesNotContainFunctionExceptionMessage = 'O módulo {0} não contém a função {1} para converter em uma Rota.' + pathToIconForGuiDoesNotExistExceptionMessage = 'O caminho para o ícone da interface gráfica não existe: {0}' + noTitleSuppliedForPageExceptionMessage = 'Nenhum título fornecido para a página {0}.' + certificateSuppliedForNonHttpsWssEndpointExceptionMessage = 'Certificado fornecido para endpoint que não é HTTPS/WSS.' + cannotLockNullObjectExceptionMessage = 'Não é possível bloquear um objeto nulo.' + showPodeGuiOnlyAvailableOnWindowsExceptionMessage = 'Show-PodeGui está atualmente disponível apenas para Windows PowerShell e PowerShell 7+ no Windows.' + unlockSecretButNoScriptBlockExceptionMessage = 'Segredo de desbloqueio fornecido para tipo de Cofre Secreto personalizado, mas nenhum ScriptBlock de desbloqueio fornecido.' + invalidIpAddressExceptionMessage = 'O endereço IP fornecido é inválido: {0}' + maxDaysInvalidExceptionMessage = 'MaxDays deve ser igual ou maior que 0, mas foi obtido: {0}' + noRemoveScriptBlockForVaultExceptionMessage = "Nenhum ScriptBlock fornecido para remover segredos do cofre '{0}'" + noSecretExpectedForNoSignatureExceptionMessage = 'Não era esperado nenhum segredo para nenhuma assinatura.' + noCertificateFoundExceptionMessage = "Nenhum certificado encontrado em {0}{1} para '{2}'" + minValueInvalidExceptionMessage = "O valor mínimo '{0}' para {1} é inválido, deve ser maior ou igual a {2}" + accessRequiresAuthenticationOnRoutesExceptionMessage = 'O acesso requer autenticação nas rotas.' + noSecretForHmac384ExceptionMessage = 'Nenhum segredo fornecido para o hash HMAC384.' + windowsLocalAuthSupportIsForWindowsOnlyExceptionMessage = 'O suporte à Autenticação Local do Windows é apenas para Windows.' + definitionTagNotDefinedExceptionMessage = 'A tag de definição {0} não existe.' + noComponentInDefinitionExceptionMessage = 'Nenhum componente do tipo {0} chamado {1} está disponível na definição {2}.' + noSmtpHandlersDefinedExceptionMessage = 'Nenhum manipulador SMTP definido.' + sessionMiddlewareAlreadyInitializedExceptionMessage = 'O Middleware de Sessão já foi inicializado.' + reusableComponentPathItemsNotAvailableInOpenApi30ExceptionMessage = "O recurso de componente reutilizável 'pathItems' não está disponível no OpenAPI v3.0." + wildcardHeadersIncompatibleWithAutoHeadersExceptionMessage = 'O caractere curinga * para os Cabeçalhos é incompatível com a chave AutoHeaders.' + noDataForFileUploadedExceptionMessage = "Nenhum dado para o arquivo '{0}' foi enviado na solicitação." + sseOnlyConfiguredOnEventStreamAcceptHeaderExceptionMessage = 'SSE só pode ser configurado em solicitações com um valor de cabeçalho Accept de text/event-stream.' + noSessionAvailableToSaveExceptionMessage = 'Não há sessão disponível para salvar.' + pathParameterRequiresRequiredSwitchExceptionMessage = "Se a localização do parâmetro for 'Path', o parâmetro de switch 'Required' é obrigatório." + noOpenApiUrlSuppliedExceptionMessage = 'Nenhuma URL do OpenAPI fornecida para {0}.' + maximumConcurrentSchedulesInvalidExceptionMessage = 'As cronogramas simultâneas máximas devem ser >=1, mas obtidas: {0}' + snapinsSupportedOnWindowsPowershellOnlyExceptionMessage = 'Os Snapins são suportados apenas no Windows PowerShell.' + eventViewerLoggingSupportedOnWindowsOnlyExceptionMessage = 'O registro no Visualizador de Eventos é suportado apenas no Windows.' + parametersMutuallyExclusiveExceptionMessage = "Os parâmetros '{0}' e '{1}' são mutuamente exclusivos." + pathItemsFeatureNotSupportedInOpenApi30ExceptionMessage = 'O recurso PathItems não é suportado no OpenAPI v3.0.x' + openApiParameterRequiresNameExceptionMessage = 'O parâmetro OpenApi requer um nome especificado.' + maximumConcurrentTasksLessThanMinimumExceptionMessage = 'O número máximo de tarefas concorrentes não pode ser menor que o mínimo de {0}, mas foi obtido: {1}' + noSemaphoreFoundExceptionMessage = "Nenhum semáforo encontrado chamado '{0}'" + singleValueForIntervalExceptionMessage = 'Você pode fornecer apenas um único valor {0} ao usar intervalos.' + jwtNotYetValidExceptionMessage = 'O JWT ainda não é válido para uso.' + verbAlreadyDefinedForUrlExceptionMessage = '[Verbo] {0}: Já definido para {1}' + noSecretNamedMountedExceptionMessage = "Nenhum Segredo com o nome '{0}' foi montado." + moduleOrVersionNotFoundExceptionMessage = 'Módulo ou versão não encontrada em {0}: {1}@{2}' + noScriptBlockSuppliedExceptionMessage = 'Nenhum ScriptBlock fornecido.' + noSecretVaultRegisteredExceptionMessage = "Nenhum Cofre de Segredos com o nome '{0}' foi registrado." + nameRequiredForEndpointIfRedirectToSuppliedExceptionMessage = 'Um nome é necessário para o endpoint se o parâmetro RedirectTo for fornecido.' + openApiLicenseObjectRequiresNameExceptionMessage = "O objeto OpenAPI 'license' requer a propriedade 'name'. Use o parâmetro -LicenseName." + sourcePathDoesNotExistForStaticRouteExceptionMessage = '{0}: O caminho de origem fornecido para a Rota Estática não existe: {1}' + noNameForWebSocketDisconnectExceptionMessage = 'Nenhum nome fornecido para desconectar do WebSocket.' + certificateExpiredExceptionMessage = "O certificado '{0}' expirou: {1}" + secretVaultUnlockExpiryDateInPastExceptionMessage = 'A data de expiração de desbloqueio do Cofre de Segredos está no passado (UTC): {0}' + invalidWebExceptionTypeExceptionMessage = 'A exceção é de um tipo inválido, deve ser WebException ou HttpRequestException, mas foi obtido: {0}' + invalidSecretValueTypeExceptionMessage = 'O valor do segredo é de um tipo inválido. Tipos esperados: String, SecureString, HashTable, Byte[] ou PSCredential. Mas obtido: {0}' + explicitTlsModeOnlySupportedOnSmtpsTcpsEndpointsExceptionMessage = 'O modo TLS explícito é suportado apenas em endpoints SMTPS e TCPS.' + discriminatorMappingRequiresDiscriminatorPropertyExceptionMessage = "O parâmetro 'DiscriminatorMapping' só pode ser usado quando 'DiscriminatorProperty' está presente." + scriptErrorExceptionMessage = "Erro '{0}' no script {1} {2} (linha {3}) caractere {4} executando {5} em {6} objeto '{7}' Classe: {8} ClasseBase: {9}" + cannotSupplyIntervalForQuarterExceptionMessage = 'Não é possível fornecer um valor de intervalo para cada trimestre.' + scheduleEndTimeMustBeInFutureExceptionMessage = '[Cronograma] {0}: O valor de EndTime deve estar no futuro.' + invalidJwtSignatureSuppliedExceptionMessage = 'Assinatura JWT fornecida inválida.' + noSetScriptBlockForVaultExceptionMessage = "Nenhum ScriptBlock fornecido para atualizar/criar segredos no cofre '{0}'" + accessMethodNotExistForMergingExceptionMessage = 'O método de acesso não existe para a mesclagem: {0}' + defaultAuthNotInListExceptionMessage = "A Autenticação Default '{0}' não está na lista de Autenticação fornecida." + parameterHasNoNameExceptionMessage = "O parâmetro não tem nome. Dê um nome a este componente usando o parâmetro 'Name'." + methodPathAlreadyDefinedForUrlExceptionMessage = '[{0}] {1}: Já definido para {2}' + fileWatcherAlreadyDefinedExceptionMessage = "Um Observador de Arquivos chamado '{0}' já foi definido." + noServiceHandlersDefinedExceptionMessage = 'Nenhum manipulador de serviço definido.' + secretRequiredForCustomSessionStorageExceptionMessage = 'Um segredo é necessário ao usar armazenamento de sessão personalizado.' + secretManagementModuleNotInstalledExceptionMessage = 'O módulo Microsoft.PowerShell.SecretManagement não está instalado.' + noPathSuppliedForRouteExceptionMessage = 'Nenhum caminho fornecido para a Rota.' + validationOfAnyOfSchemaNotSupportedExceptionMessage = "A validação de um esquema que inclui 'anyof' não é suportada." + iisAuthSupportIsForWindowsOnlyExceptionMessage = 'O suporte à Autenticação IIS é apenas para Windows.' + oauth2InnerSchemeInvalidExceptionMessage = 'O OAuth2 InnerScheme só pode ser um de autenticação Basic ou Form, mas foi obtido: {0}' + noRoutePathSuppliedForPageExceptionMessage = 'Nenhum caminho de rota fornecido para a página {0}.' + cacheStorageNotFoundForExistsExceptionMessage = "Armazenamento em cache com o nome '{0}' não encontrado ao tentar verificar se o item em cache '{1}' existe." + handlerAlreadyDefinedExceptionMessage = '[{0}] {1}: Manipulador já definido.' + sessionsNotConfiguredExceptionMessage = 'As sessões não foram configuradas.' + propertiesTypeObjectAssociationExceptionMessage = 'Apenas propriedades do tipo Objeto podem ser associadas com {0}.' + sessionsRequiredForSessionPersistentAuthExceptionMessage = 'Sessões são necessárias para usar a autenticação persistente por sessão.' + invalidPathWildcardOrDirectoryExceptionMessage = 'O caminho fornecido não pode ser um curinga ou um diretório: {0}' + accessMethodAlreadyDefinedExceptionMessage = 'Método de acesso já definido: {0}' + parametersValueOrExternalValueMandatoryExceptionMessage = "Os parâmetros 'Value' ou 'ExternalValue' são obrigatórios." + maximumConcurrentTasksInvalidExceptionMessage = 'O número máximo de tarefas concorrentes deve ser >=1, mas foi obtido: {0}' + cannotCreatePropertyWithoutTypeExceptionMessage = 'Não é possível criar a propriedade porque nenhum tipo é definido.' + authMethodNotExistForMergingExceptionMessage = 'O método de autenticação não existe para mesclagem: {0}' + maxValueInvalidExceptionMessage = "O valor máximo '{0}' para {1} é inválido, deve ser menor ou igual a {2}" + endpointAlreadyDefinedExceptionMessage = "Um ponto de extremidade chamado '{0}' já foi definido." + eventAlreadyRegisteredExceptionMessage = 'Evento {0} já registrado: {1}' + parameterNotSuppliedInRequestExceptionMessage = "Um parâmetro chamado '{0}' não foi fornecido na solicitação ou não há dados disponíveis." + cacheStorageNotFoundForSetExceptionMessage = "Armazenamento em cache com o nome '{0}' não encontrado ao tentar definir o item em cache '{1}'." + methodPathAlreadyDefinedExceptionMessage = '[{0}] {1}: Já definido.' + errorLoggingAlreadyEnabledExceptionMessage = 'O registro de erros já está habilitado.' + valueForUsingVariableNotFoundExceptionMessage = "Valor para '`$using:{0}' não pôde ser encontrado." + rapidPdfDoesNotSupportOpenApi31ExceptionMessage = 'A ferramenta de documentos RapidPdf não suporta OpenAPI 3.1' + oauth2ClientSecretRequiredExceptionMessage = 'OAuth2 requer um Client Secret quando não se usa PKCE.' + invalidBase64JwtExceptionMessage = 'Valor codificado Base64 inválido encontrado no JWT' + noSessionToCalculateDataHashExceptionMessage = 'Nenhuma sessão disponível para calcular o hash dos dados.' + cacheStorageNotFoundForRemoveExceptionMessage = "Armazenamento em cache com o nome '{0}' não encontrado ao tentar remover o item em cache '{1}'." + csrfMiddlewareNotInitializedExceptionMessage = 'O Middleware CSRF não foi inicializado.' + infoTitleMandatoryMessage = 'info.title é obrigatório.' + typeCanOnlyBeAssociatedWithObjectExceptionMessage = 'O tipo {0} só pode ser associado a um Objeto.' + userFileDoesNotExistExceptionMessage = 'O arquivo do usuário não existe: {0}' + routeParameterNeedsValidScriptblockExceptionMessage = 'O parâmetro da Rota precisa de um ScriptBlock válido e não vazio.' + nextTriggerCalculationErrorExceptionMessage = 'Parece que algo deu errado ao tentar calcular a próxima data e hora do gatilho: {0}' + cannotLockValueTypeExceptionMessage = 'Não é possível bloquear um [ValueType].' + failedToCreateOpenSslCertExceptionMessage = 'Falha ao criar o certificado OpenSSL: {0}' + jwtExpiredExceptionMessage = 'O JWT expirou.' + openingGuiMessage = 'Abrindo a GUI.' + multiTypePropertiesRequireOpenApi31ExceptionMessage = 'Propriedades de múltiplos tipos requerem a versão 3.1 ou superior do OpenApi.' + noNameForWebSocketRemoveExceptionMessage = 'Nenhum nome fornecido para remover o WebSocket.' + maxSizeInvalidExceptionMessage = 'MaxSize deve ser igual ou maior que 0, mas foi obtido: {0}' + iisShutdownMessage = '(Desligamento do IIS)' + cannotUnlockValueTypeExceptionMessage = 'Não é possível desbloquear um [ValueType].' + noJwtSignatureForAlgorithmExceptionMessage = 'Nenhuma assinatura JWT fornecida para {0}.' + maximumConcurrentWebSocketThreadsInvalidExceptionMessage = 'O número máximo de threads concorrentes do WebSocket deve ser >=1, mas foi obtido: {0}' + acknowledgeMessageOnlySupportedOnSmtpTcpEndpointsExceptionMessage = 'A mensagem de reconhecimento é suportada apenas em endpoints SMTP e TCP.' + failedToConnectToUrlExceptionMessage = 'Falha ao conectar ao URL: {0}' + failedToAcquireMutexOwnershipExceptionMessage = 'Falha ao adquirir a propriedade do mutex. Nome do mutex: {0}' + sessionsRequiredForOAuth2WithPKCEExceptionMessage = 'Sessões são necessárias para usar OAuth2 com PKCE' + failedToConnectToWebSocketExceptionMessage = 'Falha ao conectar ao WebSocket: {0}' + unsupportedObjectExceptionMessage = 'Objeto não suportado' + failedToParseAddressExceptionMessage = "Falha ao analisar '{0}' como um endereço IP/Host:Port válido" + mustBeRunningWithAdminPrivilegesExceptionMessage = 'Deve estar sendo executado com privilégios de administrador para escutar endereços que não sejam localhost.' + specificationMessage = 'Especificação' + cacheStorageNotFoundForClearExceptionMessage = "Armazenamento em cache com o nome '{0}' não encontrado ao tentar limpar o cache." + restartingServerMessage = 'Reiniciando o servidor...' + cannotSupplyIntervalWhenEveryIsNoneExceptionMessage = "Não é possível fornecer um intervalo quando o parâmetro 'Every' está definido como None." + unsupportedJwtAlgorithmExceptionMessage = 'O algoritmo JWT não é atualmente suportado: {0}' + websocketsNotConfiguredForSignalMessagesExceptionMessage = 'WebSockets não estão configurados para enviar mensagens de sinal.' + invalidLogicTypeInHashtableMiddlewareExceptionMessage = 'Um Middleware do tipo Hashtable fornecido tem um tipo de lógica inválido. Esperado ScriptBlock, mas obtido: {0}' + maximumConcurrentSchedulesLessThanMinimumExceptionMessage = 'As cronogramas simultâneas máximas não podem ser inferiores ao mínimo de {0}, mas obtidas: {1}' + failedToAcquireSemaphoreOwnershipExceptionMessage = 'Falha ao adquirir a propriedade do semáforo. Nome do semáforo: {0}' + propertiesParameterWithoutNameExceptionMessage = 'Os parâmetros Properties não podem ser usados se a propriedade não tiver um nome.' + customSessionStorageMethodNotImplementedExceptionMessage = "O armazenamento de sessão personalizado não implementa o método requerido '{0}()'." + authenticationMethodDoesNotExistExceptionMessage = 'O método de autenticação não existe: {0}' + webhooksFeatureNotSupportedInOpenApi30ExceptionMessage = 'O recurso Webhooks não é suportado no OpenAPI v3.0.x' + invalidContentTypeForSchemaExceptionMessage = "'content-type' inválido encontrado para o esquema: {0}" + noUnlockScriptBlockForVaultExceptionMessage = "Nenhum ScriptBlock de desbloqueio fornecido para desbloquear o cofre '{0}'" + definitionTagMessage = 'Definição {0}:' + failedToOpenRunspacePoolExceptionMessage = 'Falha ao abrir o RunspacePool: {0}' + failedToCloseRunspacePoolExceptionMessage = 'Falha ao fechar RunspacePool: {0}' + verbNoLogicPassedExceptionMessage = '[Verbo] {0}: Nenhuma lógica passada' + noMutexFoundExceptionMessage = "Nenhum mutex encontrado chamado '{0}'" + documentationMessage = 'Documentação' + timerAlreadyDefinedExceptionMessage = '[Temporizador] {0}: Temporizador já definido.' + invalidPortExceptionMessage = 'A porta não pode ser negativa: {0}' + viewsFolderNameAlreadyExistsExceptionMessage = 'O nome da pasta Views já existe: {0}' + noNameForWebSocketResetExceptionMessage = 'Nenhum nome fornecido para redefinir o WebSocket.' + mergeDefaultAuthNotInListExceptionMessage = "A Autenticação MergeDefault '{0}' não está na lista de Autenticação fornecida." + descriptionRequiredExceptionMessage = 'Uma descrição é necessária para o Caminho:{0} Resposta:{1}' + pageNameShouldBeAlphaNumericExceptionMessage = 'O nome da página deve ser um valor alfanumérico válido: {0}' + defaultValueNotBooleanOrEnumExceptionMessage = 'O valor padrão não é booleano e não faz parte do enum.' + openApiComponentSchemaDoesNotExistExceptionMessage = 'O esquema do componente OpenApi {0} não existe.' + timerParameterMustBeGreaterThanZeroExceptionMessage = '[Temporizador] {0}: {1} deve ser maior que 0.' + taskTimedOutExceptionMessage = 'A tarefa expirou após {0}ms.' + scheduleStartTimeAfterEndTimeExceptionMessage = "[Cronograma] {0}: Não pode ter um 'StartTime' após o 'EndTime'" + infoVersionMandatoryMessage = 'info.version é obrigatório.' + cannotUnlockNullObjectExceptionMessage = 'Não é possível desbloquear um objeto nulo.' + nonEmptyScriptBlockRequiredForCustomAuthExceptionMessage = 'É necessário um ScriptBlock não vazio para o esquema de autenticação personalizado.' + nonEmptyScriptBlockRequiredForAuthMethodExceptionMessage = 'Um ScriptBlock não vazio é necessário para o método de autenticação.' + validationOfOneOfSchemaNotSupportedExceptionMessage = "A validação de um esquema que inclui 'oneof' não é suportada." + routeParameterCannotBeNullExceptionMessage = "O parâmetro 'Route' não pode ser nulo." + cacheStorageAlreadyExistsExceptionMessage = "Armazenamento em cache com o nome '{0}' já existe." + loggingMethodRequiresValidScriptBlockExceptionMessage = "O método de saída fornecido para o método de registro '{0}' requer um ScriptBlock válido." + scopedVariableAlreadyDefinedExceptionMessage = 'Variável de escopo já definida: {0}' + oauth2RequiresAuthorizeUrlExceptionMessage = 'OAuth2 requer que seja fornecida uma URL de Autorização' + pathNotExistExceptionMessage = 'O caminho não existe: {0}' + noDomainServerNameForWindowsAdAuthExceptionMessage = 'Nenhum nome de servidor de domínio foi fornecido para a autenticação AD do Windows' + suppliedDateAfterScheduleEndTimeExceptionMessage = 'A data fornecida é posterior ao horário de término da cronograma em {0}' + wildcardMethodsIncompatibleWithAutoMethodsExceptionMessage = 'O caractere curinga * para os Métodos é incompatível com a chave AutoMethods.' + cannotSupplyIntervalForYearExceptionMessage = 'Não é possível fornecer um valor de intervalo para cada ano.' + missingComponentsMessage = 'Componente(s) ausente(s)' + invalidStrictTransportSecurityDurationExceptionMessage = 'Duração inválida fornecida para Strict-Transport-Security: {0}. Deve ser maior que 0.' + noSecretForHmac512ExceptionMessage = 'Nenhum segredo fornecido para o hash HMAC512.' + daysInMonthExceededExceptionMessage = '{0} tem apenas {1} dias, mas {2} foi fornecido.' + nonEmptyScriptBlockRequiredForCustomLoggingExceptionMessage = 'Um ScriptBlock não vazio é necessário para o método de registro personalizado.' + encodingAttributeOnlyAppliesToMultipartExceptionMessage = 'O atributo de codificação só se aplica a corpos de solicitação multipart e application/x-www-form-urlencoded.' + suppliedDateBeforeScheduleStartTimeExceptionMessage = 'A data fornecida é anterior ao horário de início da cronograma em {0}' + unlockSecretRequiredExceptionMessage = "É necessária uma propriedade 'UnlockSecret' ao usar Microsoft.PowerShell.SecretStore" + noLogicPassedForMethodRouteExceptionMessage = '[{0}] {1}: Nenhuma lógica passada.' + bodyParserAlreadyDefinedForContentTypeExceptionMessage = 'Um body-parser já está definido para o tipo de conteúdo {0}.' + invalidJwtSuppliedExceptionMessage = 'JWT fornecido inválido.' + sessionsRequiredForFlashMessagesExceptionMessage = 'Sessões são necessárias para usar mensagens Flash.' + semaphoreAlreadyExistsExceptionMessage = 'Já existe um semáforo com o seguinte nome: {0}' + invalidJwtHeaderAlgorithmSuppliedExceptionMessage = 'Algoritmo de cabeçalho JWT fornecido inválido.' + oauth2ProviderDoesNotSupportPasswordGrantTypeExceptionMessage = "O provedor OAuth2 não suporta o grant_type 'password' necessário ao usar um InnerScheme." + invalidAliasFoundExceptionMessage = 'Alias {0} inválido encontrado: {1}' + scheduleDoesNotExistExceptionMessage = "A cronograma '{0}' não existe." + accessMethodNotExistExceptionMessage = 'O método de acesso não existe: {0}' + oauth2ProviderDoesNotSupportCodeResponseTypeExceptionMessage = "O provedor OAuth2 não suporta o response_type 'code'." + untestedPowerShellVersionWarningMessage = '[AVISO] Pode {0} não foi testado no PowerShell {1}, pois não estava disponível quando o Pode foi lançado.' + secretVaultAlreadyRegisteredAutoImportExceptionMessage = "Um Cofre de Segredos com o nome '{0}' já foi registrado durante a importação automática de Cofres de Segredos." + schemeRequiresValidScriptBlockExceptionMessage = "O esquema fornecido para o validador de autenticação '{0}' requer um ScriptBlock válido." + serverLoopingMessage = 'Looping do servidor a cada {0} segundos' + certificateThumbprintsNameSupportedOnWindowsExceptionMessage = 'Impressões digitais/nome do certificado são suportados apenas no Windows.' + sseConnectionNameRequiredExceptionMessage = "Um nome de conexão SSE é necessário, seja de -Name ou `$WebEvent.Sse.Name." + invalidMiddlewareTypeExceptionMessage = 'Um dos Middlewares fornecidos é de um tipo inválido. Esperado ScriptBlock ou Hashtable, mas obtido: {0}' + noSecretForJwtSignatureExceptionMessage = 'Nenhum segredo fornecido para a assinatura JWT.' + modulePathDoesNotExistExceptionMessage = 'O caminho do módulo não existe: {0}' + taskAlreadyDefinedExceptionMessage = '[Tarefa] {0}: Tarefa já definida.' + verbAlreadyDefinedExceptionMessage = '[Verbo] {0}: Já definido' + clientCertificatesOnlySupportedOnHttpsEndpointsExceptionMessage = 'Certificados de cliente são suportados apenas em endpoints HTTPS.' + endpointNameNotExistExceptionMessage = "O ponto de extremidade com o nome '{0}' não existe." + middlewareNoLogicSuppliedExceptionMessage = '[Middleware]: Nenhuma lógica fornecida no ScriptBlock.' + scriptBlockRequiredForMergingUsersExceptionMessage = 'É necessário um ScriptBlock para mesclar vários usuários autenticados em 1 objeto quando Valid é All.' + secretVaultAlreadyRegisteredExceptionMessage = "Um Cofre de Segredos com o nome '{0}' já foi registrado{1}." + deprecatedTitleVersionDescriptionWarningMessage = "AVISO: Título, Versão e Descrição em 'Enable-PodeOpenApi' estão obsoletos. Utilize 'Add-PodeOAInfo' em vez disso." + undefinedOpenApiReferencesMessage = 'Referências OpenAPI indefinidas:' + doneMessage = 'Concluído' + swaggerEditorDoesNotSupportOpenApi31ExceptionMessage = 'Esta versão do Swagger-Editor não suporta OpenAPI 3.1' + durationMustBeZeroOrGreaterExceptionMessage = 'A duração deve ser 0 ou maior, mas foi obtido: {0}s' + viewsPathDoesNotExistExceptionMessage = 'O caminho das Views não existe: {0}' + discriminatorIncompatibleWithAllOfExceptionMessage = "O parâmetro 'Discriminator' é incompatível com 'allOf'." + noNameForWebSocketSendMessageExceptionMessage = 'Nenhum nome fornecido para enviar mensagem ao WebSocket.' + hashtableMiddlewareNoLogicExceptionMessage = 'Um Middleware do tipo Hashtable fornecido não tem lógica definida.' + openApiInfoMessage = 'Informações OpenAPI:' + invalidSchemeForAuthValidatorExceptionMessage = "O esquema '{0}' fornecido para o validador de autenticação '{1}' requer um ScriptBlock válido." + sseFailedToBroadcastExceptionMessage = 'SSE falhou em transmitir devido ao nível de transmissão SSE definido para {0}: {1}.' + adModuleWindowsOnlyExceptionMessage = 'O módulo Active Directory está disponível apenas no Windows.' + requestLoggingAlreadyEnabledExceptionMessage = 'O registro de solicitações já está habilitado.' + invalidAccessControlMaxAgeDurationExceptionMessage = 'Duração inválida fornecida para Access-Control-Max-Age: {0}. Deve ser maior que 0.' + openApiDefinitionAlreadyExistsExceptionMessage = 'A definição OpenAPI com o nome {0} já existe.' + renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag não pode ser usado dentro de um 'ScriptBlock' Select-PodeOADefinition." + taskProcessDoesNotExistExceptionMessage = "O processo da tarefa '{0}' não existe." + scheduleProcessDoesNotExistExceptionMessage = "O processo do cronograma '{0}' não existe." + definitionTagChangeNotAllowedExceptionMessage = 'A Tag de definição para uma Route não pode ser alterada.' + getRequestBodyNotAllowedExceptionMessage = 'As operações {0} não podem ter um corpo de solicitação.' + fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "A função '{0}' não aceita uma matriz como entrada de pipeline." + unsupportedStreamCompressionEncodingExceptionMessage = 'A codificação de compressão de fluxo não é suportada.' +} \ No newline at end of file diff --git a/src/Locales/zh/Pode.psd1 b/src/Locales/zh/Pode.psd1 new file mode 100644 index 000000000..9e7652920 --- /dev/null +++ b/src/Locales/zh/Pode.psd1 @@ -0,0 +1,294 @@ +@{ + schemaValidationRequiresPowerShell610ExceptionMessage = '架构验证需要 PowerShell 版本 6.1.0 或更高版本。' + customAccessPathOrScriptBlockRequiredExceptionMessage = '对于源自自定义访问值,需要路径或 ScriptBlock。' + operationIdMustBeUniqueForArrayExceptionMessage = '操作ID: {0} 必须唯一,不能应用于数组。' + endpointNotDefinedForRedirectingExceptionMessage = "未定义用于重定向的名为 '{0}' 的端点。" + filesHaveChangedMessage = '以下文件已更改:' + iisAspnetcoreTokenMissingExceptionMessage = '缺少 IIS ASPNETCORE_TOKEN。' + minValueGreaterThanMaxExceptionMessage = '{0} 的最小值不应大于最大值。' + noLogicPassedForRouteExceptionMessage = '没有为路径传递逻辑: {0}' + scriptPathDoesNotExistExceptionMessage = '脚本路径不存在: {0}' + mutexAlreadyExistsExceptionMessage = "名为 '{0}' 的互斥量已存在。" + listeningOnEndpointsMessage = '正在监听以下 {0} 个端点 [{1} 个线程]:' + unsupportedFunctionInServerlessContextExceptionMessage = '不支持在无服务器上下文中使用 {0} 函数。' + expectedNoJwtSignatureSuppliedExceptionMessage = '预期不提供 JWT 签名。' + secretAlreadyMountedExceptionMessage = "名为'{0}'的秘密已挂载。" + failedToAcquireLockExceptionMessage = '未能获取对象的锁。' + noPathSuppliedForStaticRouteExceptionMessage = '[{0}]: 没有为静态路径提供路径。' + invalidHostnameSuppliedExceptionMessage = '提供的主机名无效: {0}' + authMethodAlreadyDefinedExceptionMessage = '身份验证方法已定义:{0}' + csrfCookieRequiresSecretExceptionMessage = "使用 CSRF 的 Cookie 时,需要一个密钥。您可以提供一个密钥或设置全局 Cookie 密钥 - (Set-PodeCookieSecret '' -Global)" + nonEmptyScriptBlockRequiredForPageRouteExceptionMessage = '创建页面路由需要非空的ScriptBlock。' + noPropertiesMutuallyExclusiveExceptionMessage = "参数'NoProperties'与'Properties'、'MinProperties'和'MaxProperties'互斥。" + incompatiblePodeDllExceptionMessage = '已加载存在不兼容的 Pode.DLL 版本 {0}。需要版本 {1}。请打开新的 Powershell/pwsh 会话并重试。' + accessMethodDoesNotExistExceptionMessage = '访问方法不存在:{0}。' + scheduleAlreadyDefinedExceptionMessage = '[计划] {0}: 计划已定义。' + secondsValueCannotBeZeroOrLessExceptionMessage = '{0} 的秒数值不能为 0 或更小。' + pathToLoadNotFoundExceptionMessage = '未找到要加载的路径 {0}: {1}' + failedToImportModuleExceptionMessage = '导入模块失败: {0}' + endpointNotExistExceptionMessage = "具有协议 '{0}' 和地址 '{1}' 或本地地址 '{2}' 的端点不存在。" + terminatingMessage = '正在终止...' + noCommandsSuppliedToConvertToRoutesExceptionMessage = '未提供要转换为路由的命令。' + invalidTaskTypeExceptionMessage = '任务类型无效,预期类型为[System.Threading.Tasks.Task]或[hashtable]。' + alreadyConnectedToWebSocketExceptionMessage = "已连接到名为 '{0}' 的 WebSocket" + crlfMessageEndCheckOnlySupportedOnTcpEndpointsExceptionMessage = 'CRLF消息结束检查仅支持TCP端点。' + testPodeOAComponentSchemaNeedToBeEnabledExceptionMessage = "必须使用 'Enable-PodeOpenApi -EnableSchemaValidation' 启用 'Test-PodeOAComponentSchema'。" + adModuleNotInstalledExceptionMessage = '未安装 Active Directory 模块。' + cronExpressionInvalidExceptionMessage = 'Cron 表达式应仅包含 5 个部分: {0}' + noSessionToSetOnResponseExceptionMessage = '没有可用的会话来设置响应。' + valueOutOfRangeExceptionMessage = "{1} 的值 '{0}' 无效,应在 {2} 和 {3} 之间" + loggingMethodAlreadyDefinedExceptionMessage = '日志记录方法已定义: {0}' + noSecretForHmac256ExceptionMessage = '未提供 HMAC256 哈希的密钥。' + eolPowerShellWarningMessage = '[警告] Pode {0} 未在 PowerShell {1} 上测试,因为它已达到 EOL。' + runspacePoolFailedToLoadExceptionMessage = '{0} RunspacePool 加载失败。' + noEventRegisteredExceptionMessage = '没有注册的 {0} 事件:{1}' + scheduleCannotHaveNegativeLimitExceptionMessage = '[计划] {0}: 不能有负数限制。' + openApiRequestStyleInvalidForParameterExceptionMessage = 'OpenApi 请求样式不能为 {0},适用于 {1} 参数。' + openApiDocumentNotCompliantExceptionMessage = 'OpenAPI 文档不符合规范。' + taskDoesNotExistExceptionMessage = "任务 '{0}' 不存在。" + scopedVariableNotFoundExceptionMessage = '未找到范围变量: {0}' + sessionsRequiredForCsrfExceptionMessage = '使用CSRF需要会话, 除非您想使用Cookie。' + nonEmptyScriptBlockRequiredForLoggingMethodExceptionMessage = '日志记录方法需要非空的ScriptBlock。' + credentialsPassedWildcardForHeadersLiteralExceptionMessage = '传递凭据时,标头的通配符 * 将被视为文字字符串,而不是通配符。' + podeNotInitializedExceptionMessage = 'Pode未初始化。' + multipleEndpointsForGuiMessage = '定义了多个端点,仅第一个将用于 GUI。' + operationIdMustBeUniqueExceptionMessage = '操作ID: {0} 必须唯一。' + invalidJsonJwtExceptionMessage = '在 JWT 中找到无效的 JSON 值' + noAlgorithmInJwtHeaderExceptionMessage = 'JWT 头中未提供算法。' + openApiVersionPropertyMandatoryExceptionMessage = 'OpenApi 版本属性是必需的。' + limitValueCannotBeZeroOrLessExceptionMessage = '{0} 的限制值不能为 0 或更小。' + timerDoesNotExistExceptionMessage = "计时器 '{0}' 不存在。" + openApiGenerationDocumentErrorMessage = 'OpenAPI 生成文档错误:' + routeAlreadyContainsCustomAccessExceptionMessage = "路由 '[{0}] {1}' 已经包含名称为 '{2}' 的自定义访问。" + maximumConcurrentWebSocketThreadsLessThanMinimumExceptionMessage = '最大并发 WebSocket 线程数不能小于最小值 {0},但获得: {1}' + middlewareAlreadyDefinedExceptionMessage = '[Middleware] {0}: 中间件已定义。' + invalidAtomCharacterExceptionMessage = '无效的原子字符: {0}' + invalidCronAtomFormatExceptionMessage = '发现无效的 cron 原子格式: {0}' + cacheStorageNotFoundForRetrieveExceptionMessage = "尝试检索缓存项 '{1}' 时,找不到名为 '{0}' 的缓存存储。" + headerMustHaveNameInEncodingContextExceptionMessage = '在编码上下文中使用时,标头必须有名称。' + moduleDoesNotContainFunctionExceptionMessage = '模块 {0} 不包含要转换为路径的函数 {1}。' + pathToIconForGuiDoesNotExistExceptionMessage = 'GUI 图标的路径不存在: {0}' + noTitleSuppliedForPageExceptionMessage = '未提供 {0} 页面的标题。' + certificateSuppliedForNonHttpsWssEndpointExceptionMessage = '为非HTTPS/WSS端点提供的证书。' + cannotLockNullObjectExceptionMessage = '无法锁定空对象。' + showPodeGuiOnlyAvailableOnWindowsExceptionMessage = 'Show-PodeGui目前仅适用于Windows PowerShell和Windows上的PowerShell 7+。' + unlockSecretButNoScriptBlockExceptionMessage = '为自定义秘密保险库类型提供了解锁密钥,但未提供解锁 ScriptBlock。' + invalidIpAddressExceptionMessage = '提供的 IP 地址无效: {0}' + maxDaysInvalidExceptionMessage = 'MaxDays 必须大于或等于 0, 但得到: {0}' + noRemoveScriptBlockForVaultExceptionMessage = "未为从保险库 '{0}' 中删除秘密提供删除 ScriptBlock。" + noSecretExpectedForNoSignatureExceptionMessage = '预期未提供签名的密钥。' + noCertificateFoundExceptionMessage = "在 {0}{1} 中找不到证书 '{2}'。" + minValueInvalidExceptionMessage = "{1} 的最小值 '{0}' 无效,应大于或等于 {2}" + accessRequiresAuthenticationOnRoutesExceptionMessage = '访问需要在路由上进行身份验证。' + noSecretForHmac384ExceptionMessage = '未提供 HMAC384 哈希的密钥。' + windowsLocalAuthSupportIsForWindowsOnlyExceptionMessage = 'Windows 本地身份验证支持仅适用于 Windows。' + definitionTagNotDefinedExceptionMessage = '定义标签 {0} 未定义。' + noComponentInDefinitionExceptionMessage = '定义中没有类型为 {0} 名称为 {1} 的组件。' + noSmtpHandlersDefinedExceptionMessage = '未定义 SMTP 处理程序。' + sessionMiddlewareAlreadyInitializedExceptionMessage = '会话中间件已初始化。' + reusableComponentPathItemsNotAvailableInOpenApi30ExceptionMessage = "OpenAPI v3.0中不支持可重用组件功能'pathItems'。" + wildcardHeadersIncompatibleWithAutoHeadersExceptionMessage = '标头的通配符 * 与 AutoHeaders 开关不兼容。' + noDataForFileUploadedExceptionMessage = "请求中未上传文件 '{0}' 的数据。" + sseOnlyConfiguredOnEventStreamAcceptHeaderExceptionMessage = 'SSE只能在Accept标头值为text/event-stream的请求上配置。' + noSessionAvailableToSaveExceptionMessage = '没有可保存的会话。' + pathParameterRequiresRequiredSwitchExceptionMessage = "如果参数位置是 'Path',则 'Required' 开关参数是必需的。" + noOpenApiUrlSuppliedExceptionMessage = '未提供 {0} 的 OpenAPI URL。' + maximumConcurrentSchedulesInvalidExceptionMessage = '最大并发计划数必须 >=1, 但得到: {0}' + snapinsSupportedOnWindowsPowershellOnlyExceptionMessage = 'Snapins 仅支持 Windows PowerShell。' + eventViewerLoggingSupportedOnWindowsOnlyExceptionMessage = '事件查看器日志记录仅支持Windows。' + parametersMutuallyExclusiveExceptionMessage = "参数 '{0}' 和 '{1}' 是互斥的。" + pathItemsFeatureNotSupportedInOpenApi30ExceptionMessage = '在 OpenAPI v3.0.x 中不支持 PathItems 功能。' + openApiParameterRequiresNameExceptionMessage = 'OpenApi 参数需要指定名称。' + maximumConcurrentTasksLessThanMinimumExceptionMessage = '最大并发任务数不能小于最小值 {0},但获得: {1}' + noSemaphoreFoundExceptionMessage = "找不到名为 '{0}' 的信号量" + singleValueForIntervalExceptionMessage = '当使用间隔时,只能提供单个 {0} 值。' + jwtNotYetValidExceptionMessage = 'JWT 尚未有效。' + verbAlreadyDefinedForUrlExceptionMessage = '[Verb] {0}: 已经为 {1} 定义' + noSecretNamedMountedExceptionMessage = "没有挂载名为'{0}'的秘密。" + moduleOrVersionNotFoundExceptionMessage = '在 {0} 上找不到模块或版本: {1}@{2}' + noScriptBlockSuppliedExceptionMessage = '未提供脚本块。' + noSecretVaultRegisteredExceptionMessage = "未注册名为 '{0}' 的秘密保险库。" + nameRequiredForEndpointIfRedirectToSuppliedExceptionMessage = '如果提供了RedirectTo参数, 则需要为端点指定名称。' + openApiLicenseObjectRequiresNameExceptionMessage = "OpenAPI 对象 'license' 需要属性 'name'。请使用 -LicenseName 参数。" + sourcePathDoesNotExistForStaticRouteExceptionMessage = '{0}: 为静态路径提供的源路径不存在: {1}' + noNameForWebSocketDisconnectExceptionMessage = '没有提供断开连接的 WebSocket 的名称。' + certificateExpiredExceptionMessage = "证书 '{0}' 已过期: {1}" + secretVaultUnlockExpiryDateInPastExceptionMessage = '秘密保险库的解锁到期日期已过 (UTC) :{0}' + invalidWebExceptionTypeExceptionMessage = '异常类型无效,应为 WebException 或 HttpRequestException, 但得到了: {0}' + invalidSecretValueTypeExceptionMessage = '密钥值是无效的类型。期望类型: 字符串、SecureString、HashTable、Byte[] 或 PSCredential。但得到了: {0}' + explicitTlsModeOnlySupportedOnSmtpsTcpsEndpointsExceptionMessage = '显式TLS模式仅支持SMTPS和TCPS端点。' + discriminatorMappingRequiresDiscriminatorPropertyExceptionMessage = "参数'DiscriminatorMapping'只能在存在'DiscriminatorProperty'时使用。" + scriptErrorExceptionMessage = "脚本 '{0}' 在 {1} {2} (第 {3} 行) 第 {4} 个字符处执行 {5} 对象 '{7}' 的错误。类: {8} 基类: {9}" + cannotSupplyIntervalForQuarterExceptionMessage = '无法为每季度提供间隔值。' + scheduleEndTimeMustBeInFutureExceptionMessage = '[计划] {0}: EndTime 值必须在将来。' + invalidJwtSignatureSuppliedExceptionMessage = '提供的 JWT 签名无效。' + noSetScriptBlockForVaultExceptionMessage = "未为更新/创建保险库 '{0}' 中的秘密提供设置 ScriptBlock。" + accessMethodNotExistForMergingExceptionMessage = '合并时访问方法不存在: {0}' + defaultAuthNotInListExceptionMessage = "默认身份验证 '{0}' 不在提供的身份验证列表中。" + parameterHasNoNameExceptionMessage = "参数没有名称。请使用'Name'参数为此组件命名。" + methodPathAlreadyDefinedForUrlExceptionMessage = '[{0}] {1}: 已经为 {2} 定义。' + fileWatcherAlreadyDefinedExceptionMessage = "名为 '{0}' 的文件监视器已定义。" + noServiceHandlersDefinedExceptionMessage = '未定义服务处理程序。' + secretRequiredForCustomSessionStorageExceptionMessage = '使用自定义会话存储时需要一个密钥。' + secretManagementModuleNotInstalledExceptionMessage = '未安装 Microsoft.PowerShell.SecretManagement 模块。' + noPathSuppliedForRouteExceptionMessage = '未为路由提供路径。' + validationOfAnyOfSchemaNotSupportedExceptionMessage = "不支持包含 'anyof' 的模式的验证。" + iisAuthSupportIsForWindowsOnlyExceptionMessage = 'IIS 身份验证支持仅适用于 Windows。' + oauth2InnerSchemeInvalidExceptionMessage = 'OAuth2 InnerScheme 只能是 Basic 或 Form 身份验证,但得到:{0}' + noRoutePathSuppliedForPageExceptionMessage = '未提供 {0} 页面的路由路径。' + cacheStorageNotFoundForExistsExceptionMessage = "尝试检查缓存项 '{1}' 是否存在时,找不到名为 '{0}' 的缓存存储。" + handlerAlreadyDefinedExceptionMessage = '[{0}] {1}: 处理程序已定义。' + sessionsNotConfiguredExceptionMessage = '会话尚未配置。' + propertiesTypeObjectAssociationExceptionMessage = '只有 Object 类型的属性可以与 {0} 关联。' + sessionsRequiredForSessionPersistentAuthExceptionMessage = '使用会话持久性身份验证需要会话。' + invalidPathWildcardOrDirectoryExceptionMessage = '提供的路径不能是通配符或目录: {0}' + accessMethodAlreadyDefinedExceptionMessage = '访问方法已经定义: {0}' + parametersValueOrExternalValueMandatoryExceptionMessage = "参数 'Value' 或 'ExternalValue' 是必需的。" + maximumConcurrentTasksInvalidExceptionMessage = '最大并发任务数必须 >=1, 但获得: {0}' + cannotCreatePropertyWithoutTypeExceptionMessage = '无法创建属性,因为未定义类型。' + authMethodNotExistForMergingExceptionMessage = '合并时身份验证方法不存在:{0}' + maxValueInvalidExceptionMessage = "{1} 的最大值 '{0}' 无效,应小于或等于 {2}" + endpointAlreadyDefinedExceptionMessage = "名为 '{0}' 的端点已定义。" + eventAlreadyRegisteredExceptionMessage = '{0} 事件已注册:{1}' + parameterNotSuppliedInRequestExceptionMessage = "请求中未提供名为 '{0}' 的参数或没有可用数据。" + cacheStorageNotFoundForSetExceptionMessage = "尝试设置缓存项 '{1}' 时,找不到名为 '{0}' 的缓存存储。" + methodPathAlreadyDefinedExceptionMessage = '[{0}] {1}: 已经定义。' + errorLoggingAlreadyEnabledExceptionMessage = '错误日志记录已启用。' + valueForUsingVariableNotFoundExceptionMessage = "未找到 '`$using:{0}' 的值。" + rapidPdfDoesNotSupportOpenApi31ExceptionMessage = '文档工具 RapidPdf 不支持 OpenAPI 3.1' + oauth2ClientSecretRequiredExceptionMessage = '不使用 PKCE 时, OAuth2 需要一个客户端密钥。' + invalidBase64JwtExceptionMessage = '在 JWT 中找到无效的 Base64 编码值' + noSessionToCalculateDataHashExceptionMessage = '没有可用的会话来计算数据哈希。' + cacheStorageNotFoundForRemoveExceptionMessage = "尝试删除缓存项 '{1}' 时,找不到名为 '{0}' 的缓存存储。" + csrfMiddlewareNotInitializedExceptionMessage = 'CSRF中间件未初始化。' + infoTitleMandatoryMessage = 'info.title 是必填项。' + typeCanOnlyBeAssociatedWithObjectExceptionMessage = '类型{0}只能与对象关联。' + userFileDoesNotExistExceptionMessage = '用户文件不存在:{0}' + routeParameterNeedsValidScriptblockExceptionMessage = '路由参数需要有效且非空的ScriptBlock。' + nextTriggerCalculationErrorExceptionMessage = '似乎在尝试计算下一个触发器日期时间时出现了问题: {0}' + cannotLockValueTypeExceptionMessage = '无法锁定[ValueType]。' + failedToCreateOpenSslCertExceptionMessage = '创建 OpenSSL 证书失败: {0}' + jwtExpiredExceptionMessage = 'JWT 已过期。' + openingGuiMessage = '正在打开 GUI。' + multiTypePropertiesRequireOpenApi31ExceptionMessage = '多类型属性需要 OpenApi 版本 3.1 或更高版本。' + noNameForWebSocketRemoveExceptionMessage = '没有提供要删除的 WebSocket 的名称。' + maxSizeInvalidExceptionMessage = 'MaxSize 必须大于或等于 0,但得到: {0}' + iisShutdownMessage = '(IIS 关闭)' + cannotUnlockValueTypeExceptionMessage = '无法解锁[ValueType]。' + noJwtSignatureForAlgorithmExceptionMessage = '没有为 {0} 提供 JWT 签名。' + maximumConcurrentWebSocketThreadsInvalidExceptionMessage = '最大并发 WebSocket 线程数必须 >=1, 但获得: {0}' + acknowledgeMessageOnlySupportedOnSmtpTcpEndpointsExceptionMessage = '确认消息仅支持SMTP和TCP端点。' + failedToConnectToUrlExceptionMessage = '连接到 URL 失败: {0}' + failedToAcquireMutexOwnershipExceptionMessage = '未能获得互斥量的所有权。互斥量名称: {0}' + sessionsRequiredForOAuth2WithPKCEExceptionMessage = '使用 PKCE 时需要会话来使用 OAuth2' + failedToConnectToWebSocketExceptionMessage = '连接到 WebSocket 失败: {0}' + unsupportedObjectExceptionMessage = '不支持的对象' + failedToParseAddressExceptionMessage = "无法将 '{0}' 解析为有效的 IP/主机:端口地址" + mustBeRunningWithAdminPrivilegesExceptionMessage = '必须以管理员权限运行才能监听非本地主机地址。' + specificationMessage = '规格' + cacheStorageNotFoundForClearExceptionMessage = "尝试清除缓存时,找不到名为 '{0}' 的缓存存储。" + restartingServerMessage = '正在重启服务器...' + cannotSupplyIntervalWhenEveryIsNoneExceptionMessage = "当参数'Every'设置为None时, 无法提供间隔。" + unsupportedJwtAlgorithmExceptionMessage = '当前不支持的 JWT 算法: {0}' + websocketsNotConfiguredForSignalMessagesExceptionMessage = 'WebSockets未配置为发送信号消息。' + invalidLogicTypeInHashtableMiddlewareExceptionMessage = '提供的 Hashtable 中间件具有无效的逻辑类型。期望是 ScriptBlockm, 但得到了: {0}' + maximumConcurrentSchedulesLessThanMinimumExceptionMessage = '最大并发计划数不能小于最小值 {0},但得到: {1}' + failedToAcquireSemaphoreOwnershipExceptionMessage = '未能获得信号量的所有权。信号量名称: {0}' + propertiesParameterWithoutNameExceptionMessage = '如果属性没有名称,则不能使用 Properties 参数。' + customSessionStorageMethodNotImplementedExceptionMessage = "自定义会话存储未实现所需的方法'{0}()'。" + authenticationMethodDoesNotExistExceptionMessage = '认证方法不存在: {0}' + webhooksFeatureNotSupportedInOpenApi30ExceptionMessage = '在 OpenAPI v3.0.x 中不支持 Webhooks 功能' + invalidContentTypeForSchemaExceptionMessage = "架构中发现无效的 'content-type': {0}" + noUnlockScriptBlockForVaultExceptionMessage = "未为解锁保险库 '{0}' 提供解锁 ScriptBlock。" + definitionTagMessage = '定义 {0}:' + failedToOpenRunspacePoolExceptionMessage = '打开 RunspacePool 失败: {0}' + failedToCloseRunspacePoolExceptionMessage = '无法关闭RunspacePool: {0}' + verbNoLogicPassedExceptionMessage = '[动词] {0}: 未传递逻辑' + noMutexFoundExceptionMessage = "找不到名为 '{0}' 的互斥量" + documentationMessage = '文档' + timerAlreadyDefinedExceptionMessage = '[计时器] {0}: 计时器已定义。' + invalidPortExceptionMessage = '端口不能为负数: {0}' + viewsFolderNameAlreadyExistsExceptionMessage = '视图文件夹名称已存在: {0}' + noNameForWebSocketResetExceptionMessage = '没有提供要重置的 WebSocket 的名称。' + mergeDefaultAuthNotInListExceptionMessage = "MergeDefault 身份验证 '{0}' 不在提供的身份验证列表中。" + descriptionRequiredExceptionMessage = '路径:{0} 响应:{1} 需要描述' + pageNameShouldBeAlphaNumericExceptionMessage = '页面名称应为有效的字母数字值: {0}' + defaultValueNotBooleanOrEnumExceptionMessage = '默认值不是布尔值且不属于枚举。' + openApiComponentSchemaDoesNotExistExceptionMessage = 'OpenApi 组件架构 {0} 不存在。' + timerParameterMustBeGreaterThanZeroExceptionMessage = '[计时器] {0}: {1} 必须大于 0。' + taskTimedOutExceptionMessage = '任务在 {0} 毫秒后超时。' + scheduleStartTimeAfterEndTimeExceptionMessage = "[计划] {0}: 'StartTime' 不能在 'EndTime' 之后" + infoVersionMandatoryMessage = 'info.version 是必填项。' + cannotUnlockNullObjectExceptionMessage = '无法解锁空对象。' + nonEmptyScriptBlockRequiredForCustomAuthExceptionMessage = '自定义身份验证方案需要一个非空的 ScriptBlock。' + nonEmptyScriptBlockRequiredForAuthMethodExceptionMessage = '身份验证方法需要非空的 ScriptBlock。' + validationOfOneOfSchemaNotSupportedExceptionMessage = "不支持包含 'oneof' 的模式的验证。" + routeParameterCannotBeNullExceptionMessage = "参数 'Route' 不能为空。" + cacheStorageAlreadyExistsExceptionMessage = "名为 '{0}' 的缓存存储已存在。" + loggingMethodRequiresValidScriptBlockExceptionMessage = "为 '{0}' 日志记录方法提供的输出方法需要有效的 ScriptBlock。" + scopedVariableAlreadyDefinedExceptionMessage = '已经定义了作用域变量: {0}' + oauth2RequiresAuthorizeUrlExceptionMessage = 'OAuth2 需要提供授权 URL' + pathNotExistExceptionMessage = '路径不存在: {0}' + noDomainServerNameForWindowsAdAuthExceptionMessage = '没有为 Windows AD 身份验证提供域服务器名称' + suppliedDateAfterScheduleEndTimeExceptionMessage = '提供的日期晚于计划的结束时间 {0}' + wildcardMethodsIncompatibleWithAutoMethodsExceptionMessage = '方法的通配符 * 与 AutoMethods 开关不兼容。' + cannotSupplyIntervalForYearExceptionMessage = '无法为每年提供间隔值。' + missingComponentsMessage = '缺少的组件' + invalidStrictTransportSecurityDurationExceptionMessage = '提供的严格传输安全持续时间无效: {0}。应大于 0。' + noSecretForHmac512ExceptionMessage = '未提供 HMAC512 哈希的密钥。' + daysInMonthExceededExceptionMessage = '{0} 仅有 {1} 天,但提供了 {2} 天。' + nonEmptyScriptBlockRequiredForCustomLoggingExceptionMessage = '自定义日志输出方法需要非空的ScriptBlock。' + encodingAttributeOnlyAppliesToMultipartExceptionMessage = '编码属性仅适用于 multipart 和 application/x-www-form-urlencoded 请求体。' + suppliedDateBeforeScheduleStartTimeExceptionMessage = '提供的日期早于计划的开始时间 {0}' + unlockSecretRequiredExceptionMessage = "使用 Microsoft.PowerShell.SecretStore 时需要 'UnlockSecret' 属性。" + noLogicPassedForMethodRouteExceptionMessage = '[{0}] {1}: 没有传递逻辑。' + bodyParserAlreadyDefinedForContentTypeExceptionMessage = '已为 {0} 内容类型定义了一个 body-parser。' + invalidJwtSuppliedExceptionMessage = '提供的 JWT 无效。' + sessionsRequiredForFlashMessagesExceptionMessage = '使用闪存消息需要会话。' + semaphoreAlreadyExistsExceptionMessage = "名为 '{0}' 的信号量已存在。" + invalidJwtHeaderAlgorithmSuppliedExceptionMessage = '提供的 JWT 头算法无效。' + oauth2ProviderDoesNotSupportPasswordGrantTypeExceptionMessage = "OAuth2 提供程序不支持使用 InnerScheme 所需的 'password' grant_type。" + invalidAliasFoundExceptionMessage = '找到了无效的 {0} 别名: {1}' + scheduleDoesNotExistExceptionMessage = "计划 '{0}' 不存在。" + accessMethodNotExistExceptionMessage = '访问方法不存在: {0}' + oauth2ProviderDoesNotSupportCodeResponseTypeExceptionMessage = "OAuth2 提供程序不支持 'code' response_type。" + untestedPowerShellVersionWarningMessage = '[警告] Pode {0} 未在 PowerShell {1} 上测试,因为 Pode 发布时该版本不可用。' + secretVaultAlreadyRegisteredAutoImportExceptionMessage = "已经注册了名称为 '{0}' 的秘密保险库,同时正在自动导入秘密保险库。" + schemeRequiresValidScriptBlockExceptionMessage = "提供的方案用于 '{0}' 身份验证验证器,需要一个有效的 ScriptBlock。" + serverLoopingMessage = '服务器每 {0} 秒循环一次' + certificateThumbprintsNameSupportedOnWindowsExceptionMessage = '证书指纹/名称仅在 Windows 上受支持。' + sseConnectionNameRequiredExceptionMessage = "需要SSE连接名称, 可以从-Name或`$WebEvent.Sse.Name获取。" + invalidMiddlewareTypeExceptionMessage = '提供的中间件之一是无效的类型。期望是 ScriptBlock 或 Hashtable, 但得到了: {0}' + noSecretForJwtSignatureExceptionMessage = '未提供 JWT 签名的密钥。' + modulePathDoesNotExistExceptionMessage = '模块路径不存在: {0}' + taskAlreadyDefinedExceptionMessage = '[任务] {0}: 任务已定义。' + verbAlreadyDefinedExceptionMessage = '[Verb] {0}: 已经定义' + clientCertificatesOnlySupportedOnHttpsEndpointsExceptionMessage = '客户端证书仅支持HTTPS端点。' + endpointNameNotExistExceptionMessage = "名为 '{0}' 的端点不存在。" + middlewareNoLogicSuppliedExceptionMessage = '[中间件]: ScriptBlock中未提供逻辑。' + scriptBlockRequiredForMergingUsersExceptionMessage = '当 Valid 是 All 时,需要一个 ScriptBlock 来将多个经过身份验证的用户合并为一个对象。' + secretVaultAlreadyRegisteredExceptionMessage = "名为'{0}'的秘密保险库已注册{1}。" + deprecatedTitleVersionDescriptionWarningMessage = "警告: 'Enable-PodeOpenApi' 的标题、版本和描述已被弃用。请改用 'Add-PodeOAInfo'。" + undefinedOpenApiReferencesMessage = '未定义的 OpenAPI 引用:' + doneMessage = '完成' + swaggerEditorDoesNotSupportOpenApi31ExceptionMessage = '此版本的 Swagger-Editor 不支持 OpenAPI 3.1' + durationMustBeZeroOrGreaterExceptionMessage = '持续时间必须为 0 或更大,但获得: {0}s' + viewsPathDoesNotExistExceptionMessage = '视图路径不存在: {0}' + discriminatorIncompatibleWithAllOfExceptionMessage = "参数'Discriminator'与'allOf'不兼容。" + noNameForWebSocketSendMessageExceptionMessage = '没有提供要发送消息的 WebSocket 的名称。' + hashtableMiddlewareNoLogicExceptionMessage = '提供的 Hashtable 中间件没有定义逻辑。' + openApiInfoMessage = 'OpenAPI 信息:' + invalidSchemeForAuthValidatorExceptionMessage = "提供的 '{0}' 方案用于 '{1}' 身份验证验证器,需要一个有效的 ScriptBlock。" + sseFailedToBroadcastExceptionMessage = '由于为{0}定义的SSE广播级别, SSE广播失败: {1}' + adModuleWindowsOnlyExceptionMessage = '仅支持 Windows 的 Active Directory 模块。' + requestLoggingAlreadyEnabledExceptionMessage = '请求日志记录已启用。' + invalidAccessControlMaxAgeDurationExceptionMessage = '提供的 Access-Control-Max-Age 时长无效:{0}。应大于 0。' + openApiDefinitionAlreadyExistsExceptionMessage = '名为 {0} 的 OpenAPI 定义已存在。' + renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag 不能在 Select-PodeOADefinition 'ScriptBlock' 内使用。" + taskProcessDoesNotExistExceptionMessage = "任务进程 '{0}' 不存在。" + scheduleProcessDoesNotExistExceptionMessage = "计划进程 '{0}' 不存在。" + definitionTagChangeNotAllowedExceptionMessage = 'Route的定义标签无法更改。' + getRequestBodyNotAllowedExceptionMessage = '{0} 操作不能包含请求体。' + fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "函数 '{0}' 不接受数组作为管道输入。" + unsupportedStreamCompressionEncodingExceptionMessage = '不支持的流压缩编码: {0}' +} \ No newline at end of file diff --git a/src/Misc/default-file-browsing.html.pode b/src/Misc/default-file-browsing.html.pode index 2c5c6103b..45a95ef21 100644 --- a/src/Misc/default-file-browsing.html.pode +++ b/src/Misc/default-file-browsing.html.pode @@ -6,7 +6,7 @@ " - }) - - -
- + + + $($data.Title) + + + + + + + $(if ($data.DarkMode) { + "" + }) + + + +
+ + \ No newline at end of file diff --git a/src/Pode.Internal.psd1 b/src/Pode.Internal.psd1 index 2384bc7ea..d8b1b475b 100644 --- a/src/Pode.Internal.psd1 +++ b/src/Pode.Internal.psd1 @@ -21,4 +21,5 @@ # Minimum version of the Windows PowerShell engine required by this module PowerShellVersion = '5.1' + } \ No newline at end of file diff --git a/src/Pode.psd1 b/src/Pode.psd1 index 6df13ad50..ad02ac21c 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -141,6 +141,9 @@ 'ConvertFrom-PodeXml', 'Set-PodeDefaultFolder', 'Get-PodeDefaultFolder', + 'Get-PodeCurrentRunspaceName', + 'Set-PodeCurrentRunspaceName', + 'Invoke-PodeGC', # routes 'Add-PodeRoute', @@ -184,6 +187,7 @@ 'Use-PodeSchedules', 'Test-PodeSchedule', 'Clear-PodeSchedules', + 'Get-PodeScheduleProcess', # timers 'Add-PodeTimer', @@ -207,6 +211,7 @@ 'Close-PodeTask', 'Test-PodeTaskCompleted', 'Wait-PodeTask', + 'Get-PodeTaskProcess', # middleware 'Add-PodeMiddleware', @@ -313,6 +318,7 @@ 'New-PodeOARequestBody', 'Test-PodeOADefinitionTag', 'Test-PodeOADefinition', + 'Rename-PodeOADefinitionTag', # properties 'New-PodeOAIntProperty', diff --git a/src/Pode.psm1 b/src/Pode.psm1 index 00a5be399..3e2d95553 100644 --- a/src/Pode.psm1 +++ b/src/Pode.psm1 @@ -1,59 +1,142 @@ -# root path -$root = Split-Path -Parent -Path $MyInvocation.MyCommand.Path +<# +.SYNOPSIS + Pode PowerShell Module -# load assemblies -Add-Type -AssemblyName System.Web -Add-Type -AssemblyName System.Net.Http +.DESCRIPTION + This module sets up the Pode environment, including + localization and loading necessary assemblies and functions. -# Construct the path to the module manifest (.psd1 file) -$moduleManifestPath = Join-Path -Path $root -ChildPath 'Pode.psd1' +.PARAMETER UICulture + Specifies the culture to be used for localization. -# Import the module manifest to access its properties -$moduleManifest = Import-PowerShellDataFile -Path $moduleManifestPath +.EXAMPLE + Import-Module -Name "Pode" -ArgumentList @{ UICulture = 'ko-KR' } + Sets the culture to Korean. -$podeDll = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'Pode' } +.EXAMPLE + Import-Module -Name "Pode" + Uses the default culture. -if ($podeDll) { - if ( $moduleManifest.ModuleVersion -ne '$version$') { - $moduleVersion = ([version]::new($moduleManifest.ModuleVersion + '.0')) - if ($podeDll.GetName().Version -ne $moduleVersion) { - throw "An existing incompatible Pode.DLL version $($podeDll.GetName().Version) is loaded. Version $moduleVersion is required. Open a new Powershell/pwsh session and retry." - } +.EXAMPLE + Import-Module -Name "Pode" -ArgumentList 'it-SM' + Uses the Italian San Marino region culture. + +.EXAMPLE + try { + Import-Module -Name Pode -MaximumVersion 2.99.99 + } catch { + Write-Error "Failed to load the Pode module" + throw } + The import statement is within a try/catch block. + This way, if the module fails to load, your script won’t proceed, preventing possible errors or unexpected behavior. + + .NOTES + This is the entry point for the Pode module. + +#> + +param( + [string]$UICulture +) + +# root path +$root = Split-Path -Parent -Path $MyInvocation.MyCommand.Path +$localesPath = (Join-Path -Path $root -ChildPath 'Locales') + +# Import localized messages +if ([string]::IsNullOrEmpty($UICulture)) { + $UICulture = $PsUICulture } -else { - if ($PSVersionTable.PSVersion -ge [version]'7.4.0') { - Add-Type -LiteralPath "$($root)/Libs/net8.0/Pode.dll" -ErrorAction Stop - } - elseif ($PSVersionTable.PSVersion -ge [version]'7.2.0') { - Add-Type -LiteralPath "$($root)/Libs/net6.0/Pode.dll" -ErrorAction Stop + +try { + try { + #The list of all available supported culture is available here https://azuliadesigns.com/c-sharp-tutorials/list-net-culture-country-codes/ + + # ErrorAction:SilentlyContinue is not sufficient to avoid Import-LocalizedData to generate an exception when the Culture file is not the right format + Import-LocalizedData -BindingVariable tmpPodeLocale -BaseDirectory $localesPath -UICulture $UICulture -ErrorAction:SilentlyContinue + if ($null -eq $tmpPodeLocale) { + $UICulture = 'en' + Import-LocalizedData -BindingVariable tmpPodeLocale -BaseDirectory $localesPath -UICulture $UICulture -ErrorAction:Stop + } } - else { - Add-Type -LiteralPath "$($root)/Libs/netstandard2.0/Pode.dll" -ErrorAction Stop + catch { + throw ("Failed to Import Localized Data $(Join-Path -Path $localesPath -ChildPath $UICulture -AdditionalChildPath 'Pode.psd1') $_") } -} -# load private functions -Get-ChildItem "$($root)/Private/*.ps1" | ForEach-Object { . ([System.IO.Path]::GetFullPath($_)) } + # Create the global msgTable read-only variable + New-Variable -Name 'PodeLocale' -Value $tmpPodeLocale -Scope script -Option ReadOnly -Force -Description 'Localization HashTable' + + # load assemblies + Add-Type -AssemblyName System.Web -ErrorAction Stop + Add-Type -AssemblyName System.Net.Http -ErrorAction Stop -# only import public functions -$sysfuncs = Get-ChildItem Function: + # Construct the path to the module manifest (.psd1 file) + $moduleManifestPath = Join-Path -Path $root -ChildPath 'Pode.psd1' -# only import public alias -$sysaliases = Get-ChildItem Alias: + # Import the module manifest to access its properties + $moduleManifest = Import-PowerShellDataFile -Path $moduleManifestPath -ErrorAction Stop -# load public functions -Get-ChildItem "$($root)/Public/*.ps1" | ForEach-Object { . ([System.IO.Path]::GetFullPath($_)) } -# get functions from memory and compare to existing to find new functions added -$funcs = Get-ChildItem Function: | Where-Object { $sysfuncs -notcontains $_ } -$aliases = Get-ChildItem Alias: | Where-Object { $sysaliases -notcontains $_ } -# export the module's public functions -if ($funcs) { - if ($aliases) { - Export-ModuleMember -Function ($funcs.Name) -Alias $aliases.Name + $podeDll = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'Pode' } + + if ($podeDll) { + if ( $moduleManifest.ModuleVersion -ne '$version$') { + $moduleVersion = ([version]::new($moduleManifest.ModuleVersion + '.0')) + if ($podeDll.GetName().Version -ne $moduleVersion) { + # An existing incompatible Pode.DLL version {0} is loaded. Version {1} is required. Open a new Powershell/pwsh session and retry. + throw ($PodeLocale.incompatiblePodeDllExceptionMessage -f $podeDll.GetName().Version, $moduleVersion) + } + } } else { - Export-ModuleMember -Function ($funcs.Name) + # fetch the .net version and the libs path + $version = [System.Environment]::Version.Major + $libsPath = "$($root)/Libs" + + # filter .net dll folders based on version above, and get path for latest version found + if (![string]::IsNullOrWhiteSpace($version)) { + $netFolder = Get-ChildItem -Path $libsPath -Directory -Force | + Where-Object { $_.Name -imatch "net[1-$($version)]" } | + Sort-Object -Property Name -Descending | + Select-Object -First 1 -ExpandProperty FullName + } + + # use netstandard if no folder found + if ([string]::IsNullOrWhiteSpace($netFolder)) { + $netFolder = "$($libsPath)/netstandard2.0" + } + + # append Pode.dll and mount + Add-Type -LiteralPath "$($netFolder)/Pode.dll" -ErrorAction Stop + } + + # load private functions + Get-ChildItem "$($root)/Private/*.ps1" | ForEach-Object { . ([System.IO.Path]::GetFullPath($_)) } + + # only import public functions + $sysfuncs = Get-ChildItem Function: + + # only import public alias + $sysaliases = Get-ChildItem Alias: + + # load public functions + Get-ChildItem "$($root)/Public/*.ps1" | ForEach-Object { . ([System.IO.Path]::GetFullPath($_)) } + + # get functions from memory and compare to existing to find new functions added + $funcs = Get-ChildItem Function: | Where-Object { $sysfuncs -notcontains $_ } + $aliases = Get-ChildItem Alias: | Where-Object { $sysaliases -notcontains $_ } + # export the module's public functions + if ($funcs) { + if ($aliases) { + Export-ModuleMember -Function ($funcs.Name) -Alias $aliases.Name + } + else { + Export-ModuleMember -Function ($funcs.Name) + } } } +catch { + throw ("Failed to load the Pode module. $_") +} + diff --git a/src/Private/Authentication.ps1 b/src/Private/Authentication.ps1 index 0656f4bf4..f5a2273fd 100644 --- a/src/Private/Authentication.ps1 +++ b/src/Private/Authentication.ps1 @@ -139,7 +139,7 @@ function Get-PodeAuthOAuth2Type { $result = Invoke-RestMethod -Method Post -Uri $options.Urls.Token -Body $body -ContentType 'application/x-www-form-urlencoded' -ErrorAction Stop } catch [System.Net.WebException], [System.Net.Http.HttpRequestException] { - $response = Read-PodeWebExceptionDetails -ErrorRecord $_ + $response = Read-PodeWebExceptionInfo -ErrorRecord $_ $result = ($response.Body | ConvertFrom-Json) } @@ -158,7 +158,7 @@ function Get-PodeAuthOAuth2Type { $user = Invoke-RestMethod -Method $options.Urls.User.Method -Uri $options.Urls.User.Url -Headers @{ Authorization = "Bearer $($result.access_token)" } } catch [System.Net.WebException], [System.Net.Http.HttpRequestException] { - $response = Read-PodeWebExceptionDetails -ErrorRecord $_ + $response = Read-PodeWebExceptionInfo -ErrorRecord $_ $user = ($response.Body | ConvertFrom-Json) } @@ -688,6 +688,33 @@ function Get-PodeAuthFormType { } } +<# +.SYNOPSIS + Authenticates a user based on a username and password provided as parameters. + +.DESCRIPTION + This function finds a user whose username matches the provided username, and checks the user's password. + If the password is correct, it converts the user into a hashtable and checks if the user is valid for any users/groups specified by the options parameter. If the user is valid, it returns a hashtable containing the user object. If the user is not valid, it returns a hashtable with a message indicating that the user is not authorized to access the website. + +.PARAMETER username + The username of the user to authenticate. + +.PARAMETER password + The password of the user to authenticate. + +.PARAMETER options + A hashtable containing options for the function. It can include the following keys: + - FilePath: The path to the JSON file containing user data. + - HmacSecret: The secret key for computing a HMAC-SHA256 hash of the password. + - Users: A list of valid users. + - Groups: A list of valid groups. + - ScriptBlock: A script block for additional validation. + +.EXAMPLE + Get-PodeAuthUserFileMethod -username "admin" -password "password123" -options @{ FilePath = "C:\Users.json"; HmacSecret = "secret"; Users = @("admin"); Groups = @("Administrators"); ScriptBlock = { param($user) $user.Name -eq "admin" } } + + This example authenticates a user with username "admin" and password "password123". It reads user data from the JSON file at "C:\Users.json", computes a HMAC-SHA256 hash of the password using "secret" as the secret key, and checks if the user is in the "admin" user or "Administrators" group. It also performs additional validation using a script block that checks if the user's name is "admin". +#> function Get-PodeAuthUserFileMethod { return { param($username, $password, $options) @@ -742,7 +769,7 @@ function Get-PodeAuthUserFileMethod { } # is the user valid for any users/groups? - if (!(Test-PodeAuthUserGroups -User $user -Users $_options.Users -Groups $_options.Groups)) { + if (!(Test-PodeAuthUserGroup -User $user -Users $_options.Users -Groups $_options.Groups)) { return @{ Message = 'You are not authorised to access this website' } } @@ -804,7 +831,7 @@ function Get-PodeAuthWindowsADMethod { } # is the user valid for any users/groups - if not, error! - if (!(Test-PodeAuthUserGroups -User $result.User -Users $_options.Users -Groups $_options.Groups)) { + if (!(Test-PodeAuthUserGroup -User $result.User -Users $_options.Users -Groups $_options.Groups)) { return @{ Message = 'You are not authorised to access this website' } } @@ -829,13 +856,10 @@ function Invoke-PodeAuthInbuiltScriptBlock { $ScriptBlock, [Parameter()] - $UsingVariables, - - [switch] - $NoSplat + $UsingVariables ) - return (Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -Arguments $User -UsingVariables $UsingVariables -Return -Splat:(!$NoSplat)) + return (Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -Arguments $User -UsingVariables $UsingVariables -Return) } function Get-PodeAuthWindowsLocalMethod { @@ -891,7 +915,7 @@ function Get-PodeAuthWindowsLocalMethod { } # is the user valid for any users/groups - if not, error! - if (!(Test-PodeAuthUserGroups -User $user -Users $_options.Users -Groups $_options.Groups)) { + if (!(Test-PodeAuthUserGroup -User $user -Users $_options.Users -Groups $_options.Groups)) { return @{ Message = 'You are not authorised to access this website' } } @@ -920,7 +944,7 @@ function Get-PodeAuthWindowsADIISMethod { try { # parse the auth token and get the user $winAuthToken = [System.IntPtr][Int]"0x$($token)" - $winIdentity = New-Object System.Security.Principal.WindowsIdentity($winAuthToken, 'Windows') + $winIdentity = [System.Security.Principal.WindowsIdentity]::new($winAuthToken, 'Windows') # get user and domain $username = ($winIdentity.Name -split '\\')[-1] @@ -978,7 +1002,7 @@ function Get-PodeAuthWindowsADIISMethod { # get the users groups $directGroups = $options.DirectGroups - $user.Groups = (Get-PodeAuthADGroups -Connection $connection -DistinguishedName $user.DistinguishedName -Username $user.Username -Direct:$directGroups -Provider $options.Provider) + $user.Groups = (Get-PodeAuthADGroup -Connection $connection -DistinguishedName $user.DistinguishedName -Username $user.Username -Direct:$directGroups -Provider $options.Provider) } } finally { @@ -1023,7 +1047,7 @@ function Get-PodeAuthWindowsADIISMethod { } # is the user valid for any users/groups - if not, error! - if (!(Test-PodeAuthUserGroups -User $user -Users $options.Users -Groups $options.Groups)) { + if (!(Test-PodeAuthUserGroup -User $user -Users $options.Users -Groups $options.Groups)) { return @{ Message = 'You are not authorised to access this website' } } @@ -1039,7 +1063,31 @@ function Get-PodeAuthWindowsADIISMethod { } } -function Test-PodeAuthUserGroups { +<# + .SYNOPSIS + Authenticates a user based on group membership or specific user authorization. + + .DESCRIPTION + This function checks if a given user is authorized based on supplied lists of users and groups. The user is considered authorized if their username is directly specified in the list of users, or if they are a member of any of the specified groups. + + .PARAMETER User + A hashtable representing the user, expected to contain at least the 'Username' and 'Groups' keys. + + .PARAMETER Users + An optional array of usernames. If specified, the function checks if the user's username exists in this list. + + .PARAMETER Groups + An optional array of group names. If specified, the function checks if the user belongs to any of these groups. + + .EXAMPLE + $user = @{ Username = 'john.doe'; Groups = @('Administrators', 'Users') } + $authorizedUsers = @('john.doe', 'jane.doe') + $authorizedGroups = @('Administrators') + + Test-PodeAuthUserGroup -User $user -Users $authorizedUsers -Groups $authorizedGroups + # Returns true if John Doe is either listed as an authorized user or is a member of an authorized group. +#> +function Test-PodeAuthUserGroup { param( [Parameter(Mandatory = $true)] [hashtable] @@ -1124,7 +1172,7 @@ function Invoke-PodeAuthValidation { if ($result.Success -and !$auth.PassOne) { # invoke scriptblock, or use result of merge default if ($null -ne $auth.ScriptBlock.Script) { - $result = Invoke-PodeAuthInbuiltScriptBlock -User $results -ScriptBlock $auth.ScriptBlock.Script -UsingVariables $auth.ScriptBlock.UsingVariables -NoSplat + $result = Invoke-PodeAuthInbuiltScriptBlock -User $results -ScriptBlock $auth.ScriptBlock.Script -UsingVariables $auth.ScriptBlock.UsingVariables } else { $result = $results[$auth.MergeDefault] @@ -1289,6 +1337,7 @@ function Get-PodeAuthMiddlewareScript { function Test-PodeAuthInternal { [CmdletBinding()] + [OutputType([bool])] param( [Parameter(Mandatory = $true)] [string] @@ -1369,6 +1418,8 @@ function Test-PodeAuthInternal { # did the auth force a redirect? if ($result.Redirected) { + $success = Get-PodeAuthSuccessInfo -Name $Name + Set-PodeAuthRedirectUrl -UseOrigin:($success.UseOrigin) return $false } @@ -1598,9 +1649,6 @@ function Set-PodeAuthStatus { # get auth method $auth = $PodeContext.Server.Authentications.Methods[$Name] - # cookie redirect name - $redirectCookie = 'pode.redirecturl' - # get Success object from auth $success = Get-PodeAuthSuccessInfo -Name $Name @@ -1619,10 +1667,7 @@ function Set-PodeAuthStatus { # check if we have a failure url redirect if (!$NoFailureRedirect -and ![string]::IsNullOrWhiteSpace($failure.Url)) { - if ($success.UseOrigin -and ($WebEvent.Method -ieq 'get')) { - $null = Set-PodeCookie -Name $redirectCookie -Value $WebEvent.Request.Url.PathAndQuery - } - + Set-PodeAuthRedirectUrl -UseOrigin:($success.UseOrigin) Move-PodeResponseUrl -Url $failure.Url } else { @@ -1633,20 +1678,12 @@ function Set-PodeAuthStatus { } # if no statuscode, success, so check if we have a success url redirect (but only for auto-login routes) - if ((!$NoSuccessRedirect -or $LoginRoute) -and ![string]::IsNullOrWhiteSpace($success.Url)) { - $url = $success.Url - - if ($success.UseOrigin) { - $tmpUrl = Get-PodeCookieValue -Name $redirectCookie - Remove-PodeCookie -Name $redirectCookie - - if (![string]::IsNullOrWhiteSpace($tmpUrl)) { - $url = $tmpUrl - } + if (!$NoSuccessRedirect -or $LoginRoute) { + $url = Get-PodeAuthRedirectUrl -Url $success.Url -UseOrigin:($success.UseOrigin) + if (![string]::IsNullOrWhiteSpace($url)) { + Move-PodeResponseUrl -Url $url + return $false } - - Move-PodeResponseUrl -Url $url - return $false } return $true @@ -1731,7 +1768,7 @@ function Get-PodeAuthADResult { # get the users groups $groups = @() if (!$NoGroups) { - $groups = (Get-PodeAuthADGroups -Connection $connection -DistinguishedName $user.DistinguishedName -Username $Username -Direct:$DirectGroups -Provider $Provider) + $groups = (Get-PodeAuthADGroup -Connection $connection -DistinguishedName $user.DistinguishedName -Username $Username -Direct:$DirectGroups -Provider $Provider) } # check if we want to keep the credentials in the User object @@ -1862,10 +1899,10 @@ function Open-PodeAuthADConnection { 'directoryservices' { if ([string]::IsNullOrWhiteSpace($Password)) { - $ad = (New-Object System.DirectoryServices.DirectoryEntry "$($Protocol)://$($Server)") + $ad = [System.DirectoryServices.DirectoryEntry]::new("$($Protocol)://$($Server)") } else { - $ad = (New-Object System.DirectoryServices.DirectoryEntry "$($Protocol)://$($Server)", "$($Username)", "$($Password)") + $ad = [System.DirectoryServices.DirectoryEntry]::new("$($Protocol)://$($Server)", "$($Username)", "$($Password)") } if (Test-PodeIsEmpty $ad.distinguishedName) { @@ -1938,7 +1975,7 @@ function Get-PodeAuthADUser { } 'directoryservices' { - $Connection.Searcher = New-Object System.DirectoryServices.DirectorySearcher $Connection.Entry + $Connection.Searcher = [System.DirectoryServices.DirectorySearcher]::new($Connection.Entry) $Connection.Searcher.filter = $query $result = $Connection.Searcher.FindOne().Properties @@ -1983,8 +2020,41 @@ function Get-PodeOpenLdapValue { } } } +<# +.SYNOPSIS + Retrieves Active Directory (AD) group information for a user. + +.DESCRIPTION + This function retrieves AD group information for a specified user. It supports two modes of operation: + 1. Direct: Retrieves groups directly associated with the user. + 2. All: Retrieves all groups within the specified distinguished name (DN). + +.PARAMETER Connection + The AD connection object or credentials for connecting to the AD server. + +.PARAMETER DistinguishedName + The distinguished name (DN) of the user or group. If not provided, the default DN is used. -function Get-PodeAuthADGroups { +.PARAMETER Username + The username for which to retrieve group information. + +.PARAMETER Provider + The AD provider to use (e.g., 'DirectoryServices', 'ActiveDirectory', 'OpenLDAP'). + +.PARAMETER Direct + Switch parameter. If specified, retrieves only direct group memberships for the user. + +.OUTPUTS + Returns AD group information as needed based on the mode of operation. + +.EXAMPLE + Get-PodeAuthADGroup -Connection $adConnection -Username "john.doe" + # Retrieves all AD groups for the user "john.doe". + + Get-PodeAuthADGroup -Connection $adConnection -Username "jane.smith" -Direct + # Retrieves only direct group memberships for the user "jane.smith". +#> +function Get-PodeAuthADGroup { param( [Parameter(Mandatory = $true)] $Connection, @@ -2007,13 +2077,13 @@ function Get-PodeAuthADGroups { ) if ($Direct) { - return (Get-PodeAuthADGroupsDirect -Connection $Connection -Username $Username -Provider $Provider) + return (Get-PodeAuthADGroupDirect -Connection $Connection -Username $Username -Provider $Provider) } - return (Get-PodeAuthADGroupsAll -Connection $Connection -DistinguishedName $DistinguishedName -Provider $Provider) + return (Get-PodeAuthADGroupAll -Connection $Connection -DistinguishedName $DistinguishedName -Provider $Provider) } -function Get-PodeAuthADGroupsDirect { +function Get-PodeAuthADGroupDirect { param( [Parameter(Mandatory = $true)] $Connection, @@ -2045,7 +2115,7 @@ function Get-PodeAuthADGroupsDirect { 'directoryservices' { if ($null -eq $Connection.Searcher) { - $Connection.Searcher = New-Object System.DirectoryServices.DirectorySearcher $Connection.Entry + $Connection.Searcher = [System.DirectoryServices.DirectorySearcher]::new($Connection.Entry) } $Connection.Searcher.filter = $query @@ -2062,7 +2132,7 @@ function Get-PodeAuthADGroupsDirect { return $groups } -function Get-PodeAuthADGroupsAll { +function Get-PodeAuthADGroupAll { param( [Parameter(Mandatory = $true)] $Connection, @@ -2094,7 +2164,7 @@ function Get-PodeAuthADGroupsAll { 'directoryservices' { if ($null -eq $Connection.Searcher) { - $Connection.Searcher = New-Object System.DirectoryServices.DirectorySearcher $Connection.Entry + $Connection.Searcher = [System.DirectoryServices.DirectorySearcher]::new($Connection.Entry) } $null = $Connection.Searcher.PropertiesToLoad.Add('samaccountname') @@ -2107,22 +2177,29 @@ function Get-PodeAuthADGroupsAll { } function Get-PodeAuthDomainName { - if (Test-PodeIsUnix) { - $dn = (dnsdomainname) - if ([string]::IsNullOrWhiteSpace($dn)) { - $dn = (/usr/sbin/realm list --name-only) - } + $domain = $null - return $dn + if (Test-PodeIsMacOS) { + $domain = (scutil --dns | grep -m 1 'search domain\[0\]' | cut -d ':' -f 2) + } + elseif (Test-PodeIsUnix) { + $domain = (dnsdomainname) + if ([string]::IsNullOrWhiteSpace($domain)) { + $domain = (/usr/sbin/realm list --name-only) + } } else { $domain = $env:USERDNSDOMAIN if ([string]::IsNullOrWhiteSpace($domain)) { $domain = (Get-CimInstance -Class Win32_ComputerSystem -Verbose:$false).Domain } + } - return $domain + if (![string]::IsNullOrEmpty($domain)) { + $domain = $domain.Trim() } + + return $domain } function Find-PodeAuth { @@ -2194,11 +2271,13 @@ function Expand-PodeAuthMerge { function Import-PodeAuthADModule { if (!(Test-PodeIsWindows)) { - throw 'Active Directory module only available on Windows' + # Active Directory module only available on Windows + throw ($PodeLocale.adModuleWindowsOnlyExceptionMessage) } if (!(Test-PodeModuleInstalled -Name ActiveDirectory)) { - throw 'Active Directory module is not installed' + # Active Directory module is not installed + throw ($PodeLocale.adModuleNotInstalledExceptionMessage) } Import-Module -Name ActiveDirectory -Force -ErrorAction Stop @@ -2226,4 +2305,39 @@ function Get-PodeAuthADProvider { # ds return 'DirectoryServices' +} + +function Set-PodeAuthRedirectUrl { + param( + [switch] + $UseOrigin + ) + + if ($UseOrigin -and ($WebEvent.Method -ieq 'get')) { + $null = Set-PodeCookie -Name 'pode.redirecturl' -Value $WebEvent.Request.Url.PathAndQuery + } +} + +function Get-PodeAuthRedirectUrl { + param( + [Parameter()] + [string] + $Url, + + [switch] + $UseOrigin + ) + + if (!$UseOrigin) { + return $Url + } + + $tmpUrl = Get-PodeCookieValue -Name 'pode.redirecturl' + Remove-PodeCookie -Name 'pode.redirecturl' + + if (![string]::IsNullOrWhiteSpace($tmpUrl)) { + $Url = $tmpUrl + } + + return $Url } \ No newline at end of file diff --git a/src/Private/AutoImport.ps1 b/src/Private/AutoImport.ps1 index 6af105ab7..d95c922f8 100644 --- a/src/Private/AutoImport.ps1 +++ b/src/Private/AutoImport.ps1 @@ -70,7 +70,7 @@ function Import-PodeModulesIntoRunspaceState { # work out which order the modules need to be loaded $modulesOrder = @(foreach ($module in $modules) { - Get-PodeModuleDependencies -Module $module + Get-PodeModuleDependencyList -Module $module }) | Where-Object { ($_.Name -inotin @('pode', 'pode.internal')) -and ($_.Name -inotlike 'microsoft.powershell.*') @@ -177,7 +177,8 @@ function Import-PodeSecretManagementVaultsIntoRegistry { # error if SecretManagement module not installed if (!(Test-PodeModuleInstalled -Name Microsoft.PowerShell.SecretManagement)) { - throw 'Microsoft.PowerShell.SecretManagement module not installed' + # Microsoft.PowerShell.SecretManagement module not installed + throw ($PodeLocale.secretManagementModuleNotInstalledExceptionMessage) } # import the module @@ -195,7 +196,8 @@ function Import-PodeSecretManagementVaultsIntoRegistry { # is a vault with this name already registered? if (Test-PodeSecretVault -Name $vault.Name) { - throw "A Secret Vault with the name '$($vault.Name)' has already been registered while auto-importing Secret Vaults" + throw ($PodeLocale.secretVaultAlreadyRegisteredExceptionMessage -f $vault.Name,"") + #"A Secret Vault with the name '$($vault.Name)' has already been registered while auto-importing Secret Vaults" } # register the vault diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index 457352788..4e38b56d8 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -67,30 +67,31 @@ function New-PodeContext { } # basic context object - $ctx = New-Object -TypeName psobject | - Add-Member -MemberType NoteProperty -Name Threads -Value @{} -PassThru | - Add-Member -MemberType NoteProperty -Name Timers -Value @{} -PassThru | - Add-Member -MemberType NoteProperty -Name Schedules -Value @{} -PassThru | - Add-Member -MemberType NoteProperty -Name Tasks -Value @{} -PassThru | - Add-Member -MemberType NoteProperty -Name RunspacePools -Value $null -PassThru | - Add-Member -MemberType NoteProperty -Name Runspaces -Value $null -PassThru | - Add-Member -MemberType NoteProperty -Name RunspaceState -Value $null -PassThru | - Add-Member -MemberType NoteProperty -Name Tokens -Value @{} -PassThru | - Add-Member -MemberType NoteProperty -Name LogsToProcess -Value $null -PassThru | - Add-Member -MemberType NoteProperty -Name Threading -Value @{} -PassThru | - Add-Member -MemberType NoteProperty -Name Server -Value @{} -PassThru | - Add-Member -MemberType NoteProperty -Name Metrics -Value @{} -PassThru | - Add-Member -MemberType NoteProperty -Name Listeners -Value @() -PassThru | - Add-Member -MemberType NoteProperty -Name Receivers -Value @() -PassThru | - Add-Member -MemberType NoteProperty -Name Watchers -Value @() -PassThru | - Add-Member -MemberType NoteProperty -Name Fim -Value @{} -PassThru + $ctx = [PSCustomObject]@{ + Threads = @{} + Timers = @{} + Schedules = @{} + Tasks = @{} + RunspacePools = $null + Runspaces = $null + RunspaceState = $null + Tokens = @{} + LogsToProcess = $null + Threading = @{} + Server = @{} + Metrics = @{} + Listeners = @() + Receivers = @() + Watchers = @() + Fim = @{} + } # set the server name, logic and root, and other basic properties $ctx.Server.Name = $Name $ctx.Server.Logic = $ScriptBlock $ctx.Server.LogicPath = $FilePath $ctx.Server.Interval = $Interval - $ctx.Server.PodeModule = (Get-PodeModuleDetails) + $ctx.Server.PodeModule = (Get-PodeModuleInfo) $ctx.Server.DisableTermination = $DisableTermination.IsPresent $ctx.Server.Quiet = $Quiet.IsPresent $ctx.Server.ComputerName = [System.Net.DNS]::GetHostName() @@ -116,9 +117,9 @@ function New-PodeContext { } $ctx.Tasks = @{ - Enabled = ($EnablePool -icontains 'tasks') - Items = @{} - Results = @{} + Enabled = ($EnablePool -icontains 'tasks') + Items = @{} + Processes = @{} } $ctx.Fim = @{ @@ -142,12 +143,13 @@ function New-PodeContext { Files = 1 Tasks = 2 WebSockets = 2 + Timers = 1 } # set socket details for pode server $ctx.Server.Sockets = @{ Ssl = @{ - Protocols = Get-PodeDefaultSslProtocols + Protocols = Get-PodeDefaultSslProtocol } ReceiveTimeout = 100 } @@ -275,7 +277,7 @@ function New-PodeContext { $ctx.Server.EndpointsMap = @{} # general encoding for the server - $ctx.Server.Encoding = New-Object System.Text.UTF8Encoding + $ctx.Server.Encoding = [System.Text.UTF8Encoding]::new() # setup gui details $ctx.Server.Gui = @{} @@ -373,7 +375,7 @@ function New-PodeContext { $ctx.Server.Sessions = @{} #OpenApi Definition Tag - $ctx.Server.OpenAPI = Initialize-PodeOpenApiTable -DefaultDefinitionTag $ctx.Server.Configuration.Web.OpenApi.DefaultDefinitionTag + $ctx.Server.OpenAPI = Initialize-PodeOpenApiTable -DefaultDefinitionTag $ctx.Server.Web.OpenApi.DefaultDefinitionTag # server metrics $ctx.Metrics = @{ @@ -402,12 +404,12 @@ function New-PodeContext { # create new cancellation tokens $ctx.Tokens = @{ - Cancellation = New-Object System.Threading.CancellationTokenSource - Restart = New-Object System.Threading.CancellationTokenSource + Cancellation = [System.Threading.CancellationTokenSource]::new() + Restart = [System.Threading.CancellationTokenSource]::new() } # requests that should be logged - $ctx.LogsToProcess = New-Object System.Collections.ArrayList + $ctx.LogsToProcess = [System.Collections.ArrayList]::new() # middleware that needs to run $ctx.Server.Middleware = @() @@ -432,6 +434,7 @@ function New-PodeContext { Gui = $null Tasks = $null Files = $null + Timers = $null } # threading locks, etc. @@ -493,9 +496,10 @@ function New-PodeRunspaceState { $session = New-PodeStateContext -Context $PodeContext $variables = @( - (New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'PodeContext', $session, $null), - (New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'Console', $Host, $null), - (New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'PODE_SCOPE_RUNSPACE', $true, $null) + [System.Management.Automation.Runspaces.SessionStateVariableEntry]::new('PodeLocale', $PodeLocale, $null), + [System.Management.Automation.Runspaces.SessionStateVariableEntry]::new('PodeContext', $session, $null), + [System.Management.Automation.Runspaces.SessionStateVariableEntry]::new('Console', $Host, $null), + [System.Management.Automation.Runspaces.SessionStateVariableEntry]::new('PODE_SCOPE_RUNSPACE', $true, $null) ) foreach ($var in $variables) { @@ -505,7 +509,17 @@ function New-PodeRunspaceState { $PodeContext.RunspaceState = $state } -function New-PodeRunspacePools { +<# +.SYNOPSIS + Creates and initializes runspace pools for various Pode components. + +.DESCRIPTION + This function sets up runspace pools for different Pode components, such as timers, schedules, web endpoints, web sockets, SMTP, TCP, and more. It dynamically adjusts the thread counts based on the presence of specific components and their configuration. + +.OUTPUTS + Initializes and configures runspace pools for various Pode components. +#> +function New-PodeRunspacePool { if ($PodeContext.Server.IsServerless) { return } @@ -513,16 +527,11 @@ function New-PodeRunspacePools { # setup main runspace pool $threadsCounts = @{ Default = 3 - Timer = 1 Log = 1 Schedule = 1 Misc = 1 } - if (!(Test-PodeTimersExist)) { - $threadsCounts.Timer = 0 - } - if (!(Test-PodeSchedulesExist)) { $threadsCounts.Schedule = 0 } @@ -539,7 +548,7 @@ function New-PodeRunspacePools { } # web runspace - if we have any http/s endpoints - if (Test-PodeEndpoints -Type Http) { + if (Test-PodeEndpointByProtocolType -Type Http) { $PodeContext.RunspacePools.Web = @{ Pool = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads.General + 1), $PodeContext.RunspaceState, $Host) State = 'Waiting' @@ -547,7 +556,7 @@ function New-PodeRunspacePools { } # smtp runspace - if we have any smtp endpoints - if (Test-PodeEndpoints -Type Smtp) { + if (Test-PodeEndpointByProtocolType -Type Smtp) { $PodeContext.RunspacePools.Smtp = @{ Pool = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads.General + 1), $PodeContext.RunspaceState, $Host) State = 'Waiting' @@ -555,7 +564,7 @@ function New-PodeRunspacePools { } # tcp runspace - if we have any tcp endpoints - if (Test-PodeEndpoints -Type Tcp) { + if (Test-PodeEndpointByProtocolType -Type Tcp) { $PodeContext.RunspacePools.Tcp = @{ Pool = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads.General + 1), $PodeContext.RunspaceState, $Host) State = 'Waiting' @@ -563,7 +572,7 @@ function New-PodeRunspacePools { } # signals runspace - if we have any ws/s endpoints - if (Test-PodeEndpoints -Type Ws) { + if (Test-PodeEndpointByProtocolType -Type Ws) { $PodeContext.RunspacePools.Signals = @{ Pool = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads.General + 2), $PodeContext.RunspaceState, $Host) State = 'Waiting' @@ -580,6 +589,14 @@ function New-PodeRunspacePools { New-PodeWebSocketReceiver } + # setup timer runspace pool -if we have any timers + if (Test-PodeTimersExist) { + $PodeContext.RunspacePools.Timers = @{ + Pool = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.Timers, $PodeContext.RunspaceState, $Host) + State = 'Waiting' + } + } + # setup schedule runspace pool -if we have any schedules if (Test-PodeSchedulesExist) { $PodeContext.RunspacePools.Schedules = @{ @@ -615,7 +632,17 @@ function New-PodeRunspacePools { } } -function Open-PodeRunspacePools { +<# +.SYNOPSIS + Opens and initializes runspace pools for various Pode components. + +.DESCRIPTION + This function opens and initializes runspace pools for different Pode components, such as timers, schedules, web endpoints, web sockets, SMTP, TCP, and more. It asynchronously opens the pools and waits for them to be in the 'Opened' state. If any pool fails to open, it reports an error. + +.OUTPUTS + Opens and initializes runspace pools for various Pode components. +#> +function Open-PodeRunspacePool { if ($PodeContext.Server.IsServerless) { return } @@ -666,14 +693,24 @@ function Open-PodeRunspacePools { if ($item.Pool.RunspacePoolStateInfo.State -ieq 'broken') { $item.Pool.EndOpen($item.Result) | Out-Default - throw "Failed to open RunspacePool: $($key)" + throw ($PodeLocale.failedToOpenRunspacePoolExceptionMessage -f $key) #"Failed to open RunspacePool: $($key)" } } Write-Verbose "RunspacePools opened [duration: $(([datetime]::Now - $start).TotalSeconds)s]" } -function Close-PodeRunspacePools { +<# +.SYNOPSIS + Closes and disposes runspace pools for various Pode components. + +.DESCRIPTION + This function closes and disposes runspace pools for different Pode components, such as timers, schedules, web endpoints, web sockets, SMTP, TCP, and more. It asynchronously closes the pools and waits for them to be in the 'Closed' state. If any pool fails to close, it reports an error. + +.OUTPUTS + Closes and disposes runspace pools for various Pode components. +#> +function Close-PodeRunspacePool { if ($PodeContext.Server.IsServerless -or ($null -eq $PodeContext.RunspacePools)) { return } @@ -722,7 +759,8 @@ function Close-PodeRunspacePools { if ($item.Pool.RunspacePoolStateInfo.State -ieq 'broken') { $item.Pool.EndClose($item.Result) | Out-Default - throw "Failed to close RunspacePool: $($key)" + # Failed to close RunspacePool + throw ($PodeLocale.failedToCloseRunspacePoolExceptionMessage -f $key) } } @@ -746,18 +784,19 @@ function New-PodeStateContext { $Context ) - return (New-Object -TypeName psobject | - Add-Member -MemberType NoteProperty -Name Threads -Value $Context.Threads -PassThru | - Add-Member -MemberType NoteProperty -Name Timers -Value $Context.Timers -PassThru | - Add-Member -MemberType NoteProperty -Name Schedules -Value $Context.Schedules -PassThru | - Add-Member -MemberType NoteProperty -Name Tasks -Value $Context.Tasks -PassThru | - Add-Member -MemberType NoteProperty -Name Fim -Value $Context.Fim -PassThru | - Add-Member -MemberType NoteProperty -Name RunspacePools -Value $Context.RunspacePools -PassThru | - Add-Member -MemberType NoteProperty -Name Tokens -Value $Context.Tokens -PassThru | - Add-Member -MemberType NoteProperty -Name Metrics -Value $Context.Metrics -PassThru | - Add-Member -MemberType NoteProperty -Name LogsToProcess -Value $Context.LogsToProcess -PassThru | - Add-Member -MemberType NoteProperty -Name Threading -Value $Context.Threading -PassThru | - Add-Member -MemberType NoteProperty -Name Server -Value $Context.Server -PassThru) + return [PSCustomObject]@{ + Threads = $Context.Threads + Timers = $Context.Timers + Schedules = $Context.Schedules + Tasks = $Context.Tasks + Fim = $Context.Fim + RunspacePools = $Context.RunspacePools + Tokens = $Context.Tokens + Metrics = $Context.Metrics + LogsToProcess = $Context.LogsToProcess + Threading = $Context.Threading + Server = $Context.Server + } } function Open-PodeConfiguration { @@ -824,7 +863,7 @@ function Set-PodeServerConfiguration { # sockets if (!(Test-PodeIsEmpty $Configuration.Ssl.Protocols)) { - $Context.Server.Sockets.Ssl.Protocols = (ConvertTo-PodeSslProtocols -Protocols $Configuration.Ssl.Protocols) + $Context.Server.Sockets.Ssl.Protocols = (ConvertTo-PodeSslProtocol -Protocol $Configuration.Ssl.Protocols) } if ([int]$Configuration.ReceiveTimeout -gt 0) { @@ -904,6 +943,13 @@ function Set-PodeWebConfiguration { Compression = @{ Enabled = [bool]$Configuration.Compression.Enable } + OpenApi = @{ + DefaultDefinitionTag = [string](Protect-PodeValue -Value $Configuration.OpenApi.DefaultDefinitionTag -Default 'default') + } + } + + if ($Configuration.OpenApi -and $Configuration.OpenApi.ContainsKey('UsePodeYamlInternal')) { + $Context.Server.Web.OpenApi.UsePodeYamlInternal = $Configuration.OpenApi.UsePodeYamlInternal } # setup content type route patterns for forced content types @@ -929,6 +975,9 @@ function Set-PodeWebConfiguration { } function New-PodeAutoRestartServer { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSPossibleIncorrectComparisonWithNull', '')] + [CmdletBinding()] + param() # don't configure if not supplied, or running as serverless $config = (Get-PodeConfig) if (($null -eq $config) -or ($null -eq $config.Server.Restart) -or $PodeContext.Server.IsServerless) { @@ -969,7 +1018,18 @@ function New-PodeAutoRestartServer { } } -function Set-PodeOutputVariables { +<# +.SYNOPSIS + Sets global output variables based on the Pode server context. + +.DESCRIPTION + This function sets global output variables based on the Pode server context. It retrieves output variables from the server context and assigns them as global variables. These output variables can be accessed and used in other parts of your code. + +.OUTPUTS + Sets global output variables based on the Pode server context. + +#> +function Set-PodeOutputVariable { if (Test-PodeIsEmpty $PodeContext.Server.Output.Variables) { return } diff --git a/src/Private/CronParser.ps1 b/src/Private/CronParser.ps1 index 55336397d..a08dc139d 100644 --- a/src/Private/CronParser.ps1 +++ b/src/Private/CronParser.ps1 @@ -1,5 +1,21 @@ -function Get-PodeCronFields { - return @( +<# +.SYNOPSIS + Provides a list of cron expression fields. + +.DESCRIPTION + This function returns an array of strings representing the different fields in a cron expression. These fields include 'Minute', 'Hour', 'DayOfMonth', 'Month', and 'DayOfWeek'. + +.OUTPUTS + Returns an array of strings representing cron expression fields. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Get-PodeCronField { + [CmdletBinding()] + [OutputType([string[]])] + param() + return [string[]]@( 'Minute', 'Hour', 'DayOfMonth', @@ -8,7 +24,23 @@ function Get-PodeCronFields { ) } -function Get-PodeCronFieldConstraints { +<# +.SYNOPSIS + Provides constraints and information for cron expression fields. + +.DESCRIPTION + This function returns a hashtable containing constraints and information for various cron expression fields. It includes details such as valid ranges for minutes, hours, days of the month, months, and days of the week. + +.OUTPUTS + Returns a hashtable with constraints and information for cron expression fields. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Get-PodeCronFieldConstraint { + [CmdletBinding()] + [OutputType([hashtable])] + param() return @{ MinMax = @( @(0, 59), @@ -49,7 +81,23 @@ function Get-PodeCronPredefined { } } -function Get-PodeCronFieldAliases { +<# +.SYNOPSIS + Provides aliases for cron expression fields. + +.DESCRIPTION + This function returns a hashtable containing aliases for cron expression fields. It includes mappings for month abbreviations (e.g., 'Jan' to 1) and day of the week abbreviations (e.g., 'Sun' to 0). + +.OUTPUTS + Returns a hashtable with aliases for cron expression fields. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Get-PodeCronFieldAlias { + [CmdletBinding()] + [OutputType([hashtable])] + param() return @{ Month = @{ Jan = 1 @@ -77,193 +125,223 @@ function Get-PodeCronFieldAliases { } } -function ConvertFrom-PodeCronExpressions { - param( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string[]] - $Expressions - ) +<# +.SYNOPSIS + Converts a Pode-style cron expression into a hashtable representation. - return @(@($Expressions) | ForEach-Object { - ConvertFrom-PodeCronExpression -Expression $_ - }) -} +.DESCRIPTION + This function takes an array of Pode-style cron expressions and converts them into a hashtable format. Each hashtable represents a cron expression with its individual components. + +.PARAMETER Expression + An array of Pode-style cron expressions to convert. + +.OUTPUTS + A hashtable representing the cron expression with the following keys: + - 'Minute' + - 'Hour' + - 'DayOfMonth' + - 'Month' + - 'DayOfWeek' + +.NOTES + This is an internal function and may change in future releases of Pode. +#> function ConvertFrom-PodeCronExpression { + [CmdletBinding()] + [OutputType([hashtable])] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [string] + [string[]] $Expression ) + $cronList = @() - $Expression = $Expression.Trim() - - # check predefineds - $predef = Get-PodeCronPredefined - if (!(Test-PodeIsEmpty $predef[$Expression])) { - $Expression = $predef[$Expression] - } + foreach ($item in $Expression) { + if ([string]::IsNullOrEmpty($item)) { + continue + } + $item = $item.Trim() - # split and check atoms length - $atoms = @($Expression -isplit '\s+') - if ($atoms.Length -ne 5) { - throw "Cron expression should only consist of 5 parts: $($Expression)" - } + # check predefineds + $predef = Get-PodeCronPredefined + if (!(Test-PodeIsEmpty $predef[$item])) { + $item = $predef[$item] + } - # basic variables - $aliasRgx = '(?[a-z]{3})' - - # get cron obj and validate atoms - $fields = Get-PodeCronFields - $constraints = Get-PodeCronFieldConstraints - $aliases = Get-PodeCronFieldAliases - $cron = @{} - - for ($i = 0; $i -lt $atoms.Length; $i++) { - $_cronExp = @{ - Range = $null - Values = $null - Constraints = $null - Random = $false - WildCard = $false + # split and check atoms length + $atoms = @($item -isplit '\s+') + if ($atoms.Length -ne 5) { + # Cron expression should only consist of 5 parts + throw ($PodeLocale.cronExpressionInvalidExceptionMessage -f $Expression) } - $_atom = $atoms[$i] - $_field = $fields[$i] - $_constraint = $constraints.MinMax[$i] - $_aliases = $aliases[$_field] + # basic variables + $aliasRgx = '(?[a-z]{3})' + + # get cron obj and validate atoms + $fields = Get-PodeCronField + $constraints = Get-PodeCronFieldConstraint + $aliases = Get-PodeCronFieldAlias + $cron = @{} + + for ($i = 0; $i -lt $atoms.Length; $i++) { + $_cronExp = @{ + Range = $null + Values = $null + Constraints = $null + Random = $false + WildCard = $false + } + + $_atom = $atoms[$i] + $_field = $fields[$i] + $_constraint = $constraints.MinMax[$i] + $_aliases = $aliases[$_field] # replace day of week and months with numbers if (@('month', 'dayofweek') -icontains $_field) { while ($_atom -imatch $aliasRgx) { $_alias = $_aliases[$Matches['tag']] if ($null -eq $_alias) { - throw "Invalid $($_field) alias found: $($Matches['tag'])" + # Invalid $($_field) alias found: $($Matches['tag']) + throw ($PodeLocale.invalidAliasFoundExceptionMessage -f $_field, $Matches['tag']) } - $_atom = $_atom -ireplace $Matches['tag'], $_alias - $null = $_atom -imatch $aliasRgx + $_atom = $_atom -ireplace $Matches['tag'], $_alias + $null = $_atom -imatch $aliasRgx + } } - } # ensure atom is a valid value if (!($_atom -imatch '^[\d|/|*|\-|,r]+$')) { - throw "Invalid atom character: $($_atom)" + # Invalid atom character + throw ($PodeLocale.invalidAtomCharacterExceptionMessage -f $_atom) } - # replace * with min/max constraint - if ($_atom -ieq '*') { - $_cronExp.WildCard = $true - $_atom = ($_constraint -join '-') - } - - # parse the atom for either a literal, range, array, or interval - # literal - if ($_atom -imatch '^(\d+|r)$') { - # check if it's random - if ($_atom -ieq 'r') { - $_cronExp.Values = @(Get-Random -Minimum $_constraint[0] -Maximum ($_constraint[1] + 1)) - $_cronExp.Random = $true + # replace * with min/max constraint + if ($_atom -ieq '*') { + $_cronExp.WildCard = $true + $_atom = ($_constraint -join '-') } - else { - $_cronExp.Values = @([int]$_atom) + + # parse the atom for either a literal, range, array, or interval + # literal + if ($_atom -imatch '^(\d+|r)$') { + # check if it's random + if ($_atom -ieq 'r') { + $_cronExp.Values = @(Get-Random -Minimum $_constraint[0] -Maximum ($_constraint[1] + 1)) + $_cronExp.Random = $true + } + else { + $_cronExp.Values = @([int]$_atom) + } } - } - # range - elseif ($_atom -imatch '^(?\d+)\-(?\d+)$') { - $_cronExp.Range = @{ 'Min' = [int]($Matches['min'].Trim()); 'Max' = [int]($Matches['max'].Trim()); } - } + # range + elseif ($_atom -imatch '^(?\d+)\-(?\d+)$') { + $_cronExp.Range = @{ 'Min' = [int]($Matches['min'].Trim()); 'Max' = [int]($Matches['max'].Trim()); } + } - # array - elseif ($_atom -imatch '^[\d,]+$') { - $_cronExp.Values = [int[]](@($_atom -split ',').Trim()) - } + # array + elseif ($_atom -imatch '^[\d,]+$') { + $_cronExp.Values = [int[]](@($_atom -split ',').Trim()) + } - # interval - elseif ($_atom -imatch '(?(\d+|\*))\/(?(\d+|r))$') { - $start = $Matches['start'] - $interval = $Matches['interval'] + # interval + elseif ($_atom -imatch '(?(\d+|\*))\/(?(\d+|r))$') { + $start = $Matches['start'] + $interval = $Matches['interval'] - if ($interval -ieq '0') { - $interval = '1' - } + if ($interval -ieq '0') { + $interval = '1' + } - if ([string]::IsNullOrWhiteSpace($start) -or ($start -ieq '*')) { - $start = '0' - } + if ([string]::IsNullOrWhiteSpace($start) -or ($start -ieq '*')) { + $start = '0' + } - # set the initial trigger value - $_cronExp.Values = @([int]$start) + # set the initial trigger value + $_cronExp.Values = @([int]$start) - # check if it's random - if ($interval -ieq 'r') { - $_cronExp.Random = $true - } - else { - # loop to get all next values - $next = [int]$start + [int]$interval - while ($next -le $_constraint[1]) { - $_cronExp.Values += $next - $next += [int]$interval + # check if it's random + if ($interval -ieq 'r') { + $_cronExp.Random = $true + } + else { + # loop to get all next values + $next = [int]$start + [int]$interval + while ($next -le $_constraint[1]) { + $_cronExp.Values += $next + $next += [int]$interval + } } } - } # error else { - throw "Invalid cron atom format found: $($_atom)" + # Invalid cron atom format found + throw ($PodeLocale.invalidCronAtomFormatExceptionMessage -f $_atom) } # ensure cron expression values are valid if ($null -ne $_cronExp.Range) { if ($_cronExp.Range.Min -gt $_cronExp.Range.Max) { - throw "Min value for $($_field) should not be greater than the max value" + # Min value should not be greater than the max value + throw ($PodeLocale.minValueGreaterThanMaxExceptionMessage -f $_field) } if ($_cronExp.Range.Min -lt $_constraint[0]) { - throw "Min value '$($_cronExp.Range.Min)' for $($_field) is invalid, should be greater than/equal to $($_constraint[0])" + # Min value for $($_field) is invalid, should be greater than/equal + throw ($PodeLocale.minValueInvalidExceptionMessage -f $_cronExp.Range.Min, $_field, $_constraint[0]) } if ($_cronExp.Range.Max -gt $_constraint[1]) { - throw "Max value '$($_cronExp.Range.Max)' for $($_field) is invalid, should be less than/equal to $($_constraint[1])" + # Max value for $($_field) is invalid, should be greater than/equal + throw ($PodeLocale.maxValueInvalidExceptionMessage -f $_cronExp.Range.Max, $_field, $_constraint[1]) } } if ($null -ne $_cronExp.Values) { $_cronExp.Values | ForEach-Object { if ($_ -lt $_constraint[0] -or $_ -gt $_constraint[1]) { - throw "Value '$($_)' for $($_field) is invalid, should be between $($_constraint[0]) and $($_constraint[1])" + # Value is invalid, should be between + throw ($PodeLocale.valueOutOfRangeExceptionMessage -f $value, $_field, $_constraint[0], $_constraint[1]) } } } - # assign value - $_cronExp.Constraints = $_constraint - $cron[$_field] = $_cronExp - } + # assign value + $_cronExp.Constraints = $_constraint + $cron[$_field] = $_cronExp + } # post validation for month/days in month if (($null -ne $cron['Month'].Values) -and ($null -ne $cron['DayOfMonth'].Values)) { foreach ($mon in $cron['Month'].Values) { foreach ($day in $cron['DayOfMonth'].Values) { if ($day -gt $constraints.DaysInMonths[$mon - 1]) { - throw "$($constraints.Months[$mon - 1]) only has $($constraints.DaysInMonths[$mon - 1]) days, but $($day) was supplied" + # $($constraints.Months[$mon - 1]) only has $($constraints.DaysInMonths[$mon - 1]) days, but $($day) was supplied + throw ($PodeLocale.daysInMonthExceededExceptionMessage -f $constraints.Months[$mon - 1], $constraints.DaysInMonths[$mon - 1], $day) } } } } - # flag if this cron contains a random atom - $cron['Random'] = (($cron.Values | Where-Object { $_.Random } | Measure-Object).Count -gt 0) + # flag if this cron contains a random atom + $cron['Random'] = (($cron.Values | Where-Object { $_.Random } | Measure-Object).Count -gt 0) + + # add the cron to the list + $cronList += $cron + } - # return the parsed cron expression - return $cron + # return the cronlist + return $cronList } function Reset-PodeRandomCronExpressions { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] param( [Parameter(Mandatory = $true)] [ValidateNotNull()] @@ -308,6 +386,7 @@ function Reset-PodeRandomCronExpression { } function Test-PodeCronExpressions { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] param( [Parameter(Mandatory = $true)] [ValidateNotNull()] @@ -518,7 +597,7 @@ function Get-PodeCronNextTrigger { # before we return, make sure the time is valid if (!(Test-PodeCronExpression -Expression $Expression -DateTime $NextTime)) { - throw "Looks like something went wrong trying to calculate the next trigger datetime: $($NextTime)" + throw ($PodeLocale.nextTriggerCalculationErrorExceptionMessage -f $NextTime) #"Looks like something went wrong trying to calculate the next trigger datetime: $($NextTime)" } # if before the start or after end then return null diff --git a/src/Private/Cryptography.ps1 b/src/Private/Cryptography.ps1 index 76805e20a..8ac0b86d0 100644 --- a/src/Private/Cryptography.ps1 +++ b/src/Private/Cryptography.ps1 @@ -1,5 +1,37 @@ +<# +.SYNOPSIS + Computes an HMAC-SHA256 hash for a given value using a secret key. + +.DESCRIPTION + This function calculates an HMAC-SHA256 hash for the specified value using either a secret provided as a string or as a byte array. It supports two parameter sets: + 1. String: The secret is provided as a string. + 2. Bytes: The secret is provided as a byte array. + +.PARAMETER Value + The value for which the HMAC-SHA256 hash needs to be computed. + +.PARAMETER Secret + The secret key as a string. If this parameter is provided, it will be converted to a byte array. + +.PARAMETER SecretBytes + The secret key as a byte array. If this parameter is provided, it will be used directly. + +.OUTPUTS + Returns the computed HMAC-SHA256 hash as a base64-encoded string. + +.EXAMPLE + $value = "MySecretValue" + $secret = "MySecretKey" + $hash = Invoke-PodeHMACSHA256Hash -Value $value -Secret $secret + Write-PodeHost "HMAC-SHA256 hash: $hash" + + This example computes the HMAC-SHA256 hash for the value "MySecretValue" using the secret key "MySecretKey". +.NOTES + - This function is intended for internal use. +#> function Invoke-PodeHMACSHA256Hash { [CmdletBinding(DefaultParameterSetName = 'String')] + [OutputType([String])] param( [Parameter(Mandatory = $true)] [string] @@ -14,20 +46,57 @@ function Invoke-PodeHMACSHA256Hash { $SecretBytes ) + # Convert secret to byte array if provided as a string if (![string]::IsNullOrWhiteSpace($Secret)) { $SecretBytes = [System.Text.Encoding]::UTF8.GetBytes($Secret) } + # Validate secret length if ($SecretBytes.Length -eq 0) { - throw 'No secret supplied for HMAC256 hash' + # No secret supplied for HMAC256 hash + throw ($PodeLocale.noSecretForHmac256ExceptionMessage) } + # Compute HMAC-SHA384 hash $crypto = [System.Security.Cryptography.HMACSHA256]::new($SecretBytes) return [System.Convert]::ToBase64String($crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Value))) } +<# +.SYNOPSIS + Computes a private HMAC-SHA384 hash for a given value using a secret key. + +.DESCRIPTION + This function calculates a private HMAC-SHA384 hash for the specified value using either a secret provided as a string or as a byte array. It supports two parameter sets: + 1. String: The secret is provided as a string. + 2. Bytes: The secret is provided as a byte array. + +.PARAMETER Value + The value for which the private HMAC-SHA384 hash needs to be computed. + +.PARAMETER Secret + The secret key as a string. If this parameter is provided, it will be converted to a byte array. + +.PARAMETER SecretBytes + The secret key as a byte array. If this parameter is provided, it will be used directly. + +.OUTPUTS + Returns the computed private HMAC-SHA384 hash as a base64-encoded string. + +.EXAMPLE + $value = "MySecretValue" + $secret = "MySecretKey" + $hash = Invoke-PodeHMACSHA384Hash -Value $value -Secret $secret + Write-PodeHost "Private HMAC-SHA384 hash: $hash" + + This example computes the private HMAC-SHA384 hash for the value "MySecretValue" using the secret key "MySecretKey". + +.NOTES + - This function is intended for internal use. +#> function Invoke-PodeHMACSHA384Hash { [CmdletBinding(DefaultParameterSetName = 'String')] + [OutputType([String])] param( [Parameter(Mandatory = $true)] [string] @@ -42,20 +111,57 @@ function Invoke-PodeHMACSHA384Hash { $SecretBytes ) + # Convert secret to byte array if provided as a string if (![string]::IsNullOrWhiteSpace($Secret)) { $SecretBytes = [System.Text.Encoding]::UTF8.GetBytes($Secret) } + # Validate secret length if ($SecretBytes.Length -eq 0) { - throw 'No secret supplied for HMAC384 hash' + # No secret supplied for HMAC384 hash + throw ($PodeLocale.noSecretForHmac384ExceptionMessage) } + # Compute private HMAC-SHA384 hash $crypto = [System.Security.Cryptography.HMACSHA384]::new($SecretBytes) return [System.Convert]::ToBase64String($crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Value))) } +<# +.SYNOPSIS + Computes a private HMAC-SHA512 hash for a given value using a secret key. + +.DESCRIPTION + This function calculates a private HMAC-SHA512 hash for the specified value using either a secret provided as a string or as a byte array. It supports two parameter sets: + 1. String: The secret is provided as a string. + 2. Bytes: The secret is provided as a byte array. + +.PARAMETER Value + The value for which the private HMAC-SHA512 hash needs to be computed. + +.PARAMETER Secret + The secret key as a string. If this parameter is provided, it will be converted to a byte array. + +.PARAMETER SecretBytes + The secret key as a byte array. If this parameter is provided, it will be used directly. + +.OUTPUTS + Returns the computed private HMAC-SHA512 hash as a base64-encoded string. + +.EXAMPLE + $value = "MySecretValue" + $secret = "MySecretKey" + $hash = Invoke-PodeHMACSHA512Hash -Value $value -Secret $secret + Write-PodeHost "Private HMAC-SHA512 hash: $hash" + + This example computes the private HMAC-SHA512 hash for the value "MySecretValue" using the secret key "MySecretKey". + +.NOTES + - This function is intended for internal use. +#> function Invoke-PodeHMACSHA512Hash { [CmdletBinding(DefaultParameterSetName = 'String')] + [OutputType([string])] param( [Parameter(Mandatory = $true)] [string] @@ -70,19 +176,25 @@ function Invoke-PodeHMACSHA512Hash { $SecretBytes ) + # Convert secret to byte array if provided as a string if (![string]::IsNullOrWhiteSpace($Secret)) { $SecretBytes = [System.Text.Encoding]::UTF8.GetBytes($Secret) } + # Validate secret length if ($SecretBytes.Length -eq 0) { - throw 'No secret supplied for HMAC512 hash' + # No secret supplied for HMAC512 hash + throw ($PodeLocale.noSecretForHmac512ExceptionMessage) } + # Compute private HMAC-SHA512 hash $crypto = [System.Security.Cryptography.HMACSHA512]::new($SecretBytes) return [System.Convert]::ToBase64String($crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Value))) } function Invoke-PodeSHA256Hash { + [CmdletBinding()] + [OutputType([string])] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] @@ -95,6 +207,8 @@ function Invoke-PodeSHA256Hash { } function Invoke-PodeSHA1Hash { + [CmdletBinding()] + [OutputType([string])] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] @@ -107,6 +221,8 @@ function Invoke-PodeSHA1Hash { } function ConvertTo-PodeBase64Auth { + [CmdletBinding()] + [OutputType([string])] param( [Parameter(Mandatory = $true)] [string] @@ -121,6 +237,8 @@ function ConvertTo-PodeBase64Auth { } function Invoke-PodeMD5Hash { + [CmdletBinding()] + [OutputType([string])] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] @@ -132,7 +250,25 @@ function Invoke-PodeMD5Hash { return [System.BitConverter]::ToString($crypto.ComputeHash([System.Text.Encoding]::ASCII.GetBytes($Value))).Replace('-', '').ToLowerInvariant() } -function Get-PodeRandomBytes { +<# +.SYNOPSIS +Generates a random byte array of specified length. + +.DESCRIPTION +This function generates a random byte array using the .NET `System.Security.Cryptography.RandomNumberGenerator` class. You can specify the desired length of the byte array. + +.PARAMETER Length +The length of the byte array to generate (default is 16). + +.OUTPUTS +An array of bytes representing the random byte array. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Get-PodeRandomByte { + [CmdletBinding()] + [OutputType([System.Object[]])] param( [Parameter()] [int] @@ -148,17 +284,21 @@ function Get-PodeRandomBytes { } function New-PodeSalt { + [CmdletBinding()] + [OutputType([string])] param( [Parameter()] [int] $Length = 8 ) - $bytes = [byte[]](Get-PodeRandomBytes -Length $Length) + $bytes = [byte[]](Get-PodeRandomByte -Length $Length) return [System.Convert]::ToBase64String($bytes) } function New-PodeGuid { + [CmdletBinding()] + [OutputType([string])] param( [Parameter()] [int] @@ -173,7 +313,7 @@ function New-PodeGuid { # generate a cryptographically secure guid if ($Secure) { - $bytes = [byte[]](Get-PodeRandomBytes -Length $Length) + $bytes = [byte[]](Get-PodeRandomByte -Length $Length) $guid = ([guid]::new($bytes)).ToString() } @@ -190,6 +330,8 @@ function New-PodeGuid { } function Invoke-PodeValueSign { + [CmdletBinding()] + [OutputType([string])] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [ValidateNotNullOrEmpty()] @@ -204,15 +346,18 @@ function Invoke-PodeValueSign { [switch] $Strict ) + process { + if ($Strict) { + $Secret = ConvertTo-PodeStrictSecret -Secret $Secret + } - if ($Strict) { - $Secret = ConvertTo-PodeStrictSecret -Secret $Secret + return "s:$($Value).$(Invoke-PodeHMACSHA256Hash -Value $Value -Secret $Secret)" } - - return "s:$($Value).$(Invoke-PodeHMACSHA256Hash -Value $Value -Secret $Secret)" } function Invoke-PodeValueUnsign { + [CmdletBinding()] + [OutputType([string])] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [ValidateNotNullOrEmpty()] @@ -227,35 +372,38 @@ function Invoke-PodeValueUnsign { [switch] $Strict ) + process { + # the signed value must start with "s:" + if (!$Value.StartsWith('s:')) { + return $null + } - # the signed value must start with "s:" - if (!$Value.StartsWith('s:')) { - return $null - } + # the signed value must contain a dot - splitting value and signature + $Value = $Value.Substring(2) + $periodIndex = $Value.LastIndexOf('.') + if ($periodIndex -eq -1) { + return $null + } - # the signed value must contain a dot - splitting value and signature - $Value = $Value.Substring(2) - $periodIndex = $Value.LastIndexOf('.') - if ($periodIndex -eq -1) { - return $null - } + if ($Strict) { + $Secret = ConvertTo-PodeStrictSecret -Secret $Secret + } - if ($Strict) { - $Secret = ConvertTo-PodeStrictSecret -Secret $Secret - } + # get the raw value and signature + $raw = $Value.Substring(0, $periodIndex) + $sig = $Value.Substring($periodIndex + 1) - # get the raw value and signature - $raw = $Value.Substring(0, $periodIndex) - $sig = $Value.Substring($periodIndex + 1) + if ((Invoke-PodeHMACSHA256Hash -Value $raw -Secret $Secret) -ne $sig) { + return $null + } - if ((Invoke-PodeHMACSHA256Hash -Value $raw -Secret $Secret) -ne $sig) { - return $null + return $raw } - - return $raw } function Test-PodeValueSigned { + [CmdletBinding()] + [OutputType([bool])] param( [Parameter(ValueFromPipeline = $true)] [string] @@ -269,13 +417,14 @@ function Test-PodeValueSigned { [switch] $Strict ) + process { + if ([string]::IsNullOrEmpty($Value)) { + return $false + } - if ([string]::IsNullOrEmpty($Value)) { - return $false + $result = Invoke-PodeValueUnsign -Value $Value -Secret $Secret -Strict:$Strict + return ![string]::IsNullOrEmpty($result) } - - $result = Invoke-PodeValueUnsign -Value $Value -Secret $Secret -Strict:$Strict - return ![string]::IsNullOrEmpty($result) } function ConvertTo-PodeStrictSecret { @@ -289,6 +438,8 @@ function ConvertTo-PodeStrictSecret { } function New-PodeJwtSignature { + [CmdletBinding()] + [OutputType([string])] param( [Parameter(Mandatory = $true)] [string] @@ -304,11 +455,13 @@ function New-PodeJwtSignature { ) if (($Algorithm -ine 'none') -and (($null -eq $SecretBytes) -or ($SecretBytes.Length -eq 0))) { - throw 'No Secret supplied for JWT signature' + # No secret supplied for JWT signature + throw ($PodeLocale.noSecretForJwtSignatureExceptionMessage) } if (($Algorithm -ieq 'none') -and (($null -ne $secretBytes) -and ($SecretBytes.Length -gt 0))) { - throw 'Expected no secret to be supplied for no signature' + # Expected no secret to be supplied for no signature + throw ($PodeLocale.noSecretExpectedForNoSignatureExceptionMessage) } $sig = $null @@ -334,7 +487,7 @@ function New-PodeJwtSignature { } default { - throw "The JWT algorithm is not currently supported: $($Algorithm)" + throw ($PodeLocale.unsupportedJwtAlgorithmExceptionMessage -f $Algorithm) #"The JWT algorithm is not currently supported: $($Algorithm)" } } @@ -342,6 +495,8 @@ function New-PodeJwtSignature { } function ConvertTo-PodeBase64UrlValue { + [CmdletBinding()] + [OutputType([string])] param( [Parameter(Mandatory = $true)] [string] @@ -363,6 +518,8 @@ function ConvertTo-PodeBase64UrlValue { } function ConvertFrom-PodeJwtBase64Value { + [CmdletBinding()] + [OutputType([pscustomobject])] param( [Parameter(Mandatory = $true)] [string] @@ -393,7 +550,8 @@ function ConvertFrom-PodeJwtBase64Value { $Value = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Value)) } catch { - throw 'Invalid Base64 encoded value found in JWT' + # Invalid Base64 encoded value found in JWT + throw ($PodeLocale.invalidBase64JwtExceptionMessage) } # return json @@ -401,6 +559,7 @@ function ConvertFrom-PodeJwtBase64Value { return ($Value | ConvertFrom-Json) } catch { - throw 'Invalid JSON value found in JWT' + # Invalid JSON value found in JWT + throw ($PodeLocale.invalidJsonJwtExceptionMessage) } } \ No newline at end of file diff --git a/src/Private/Endpoints.ps1 b/src/Private/Endpoints.ps1 index 0e0f6c3c7..7cc6cf657 100644 --- a/src/Private/Endpoints.ps1 +++ b/src/Private/Endpoints.ps1 @@ -1,4 +1,31 @@ -function Find-PodeEndpoints { +<# +.SYNOPSIS + Finds Pode endpoints based on protocol, address, or endpoint name. + +.DESCRIPTION + This function allows you to search for Pode endpoints based on different criteria. You can specify the protocol (HTTP or HTTPS), the address, or the endpoint name. It returns an array of hashtable objects representing the matching endpoints. + +.PARAMETER Protocol + The protocol of the endpoint (HTTP or HTTPS). + +.PARAMETER Address + The address of the endpoint. + +.PARAMETER EndpointName + The name of the endpoint. + +.OUTPUTS + An array of hashtables representing the matching endpoints, with the following keys: + - 'Protocol' + - 'Address' + - 'Name' + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Find-PodeEndpoint { + [CmdletBinding()] + [OutputType([hashtable[]])] param( [Parameter()] [ValidateSet('', 'Http', 'Https')] @@ -50,7 +77,33 @@ function Find-PodeEndpoints { return $endpoints } -function Get-PodeEndpoints { +<# +.SYNOPSIS + Retrieves internal endpoints based on the specified types. + +.DESCRIPTION + The `Get-PodeEndpointByProtocolType` function returns internal endpoints from the PodeContext + based on the specified types (HTTP, WebSocket, SMTP, or TCP). + +.PARAMETER Type + Specifies the type of endpoints to retrieve. Valid values are 'Http', 'Ws', 'Smtp', and 'Tcp'. + This parameter is mandatory. + +.OUTPUTS + Returns an array of internal endpoints matching the specified types. + +.EXAMPLE + # Example usage: + $httpEndpoints = Get-PodeEndpointByProtocolType -Type 'Http' + $wsEndpoints = Get-PodeEndpointByProtocolType -Type 'Ws' + # Retrieve HTTP and WebSocket endpoints from the PodeContext. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Get-PodeEndpointByProtocolType { + [CmdletBinding()] + [OutputType([object[]])] param( [Parameter(Mandatory = $true)] [ValidateSet('Http', 'Ws', 'Smtp', 'Tcp')] @@ -83,7 +136,7 @@ function Get-PodeEndpoints { return $endpoints } -function Test-PodeEndpointProtocol { +function Test-PodeEndpointByProtocolTypeProtocol { param( [Parameter(Mandatory = $true)] [ValidateSet('Http', 'Https', 'Ws', 'Wss', 'Smtp', 'Smtps', 'Tcp', 'Tcps')] @@ -157,7 +210,25 @@ function Get-PodeEndpointRunspacePoolName { } } -function Test-PodeEndpoints { +<# +.SYNOPSIS +Tests whether Pode endpoints of a specified type exist. + +.DESCRIPTION +This function checks if there are any Pode endpoints of the specified type (HTTP, WebSocket, SMTP, or TCP). It returns a boolean value indicating whether endpoints of that type are available. + +.PARAMETER Type +The type of Pode endpoint to test (HTTP, WebSocket, SMTP, or TCP). + +.OUTPUTS +A boolean value (True if endpoints exist, False otherwise). + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Test-PodeEndpointByProtocolType { + [CmdletBinding()] + [OutputType([bool])] param( [Parameter(Mandatory = $true)] [ValidateSet('Http', 'Ws', 'Smtp', 'Tcp')] @@ -165,7 +236,7 @@ function Test-PodeEndpoints { $Type ) - $endpoints = (Get-PodeEndpoints -Type $Type) + $endpoints = (Get-PodeEndpointByProtocolType -Type $Type) return (($null -ne $endpoints) -and ($endpoints.Length -gt 0)) } @@ -282,7 +353,7 @@ function Find-PodeEndpointName { # error? if ($ThrowError) { - throw "Endpoint with protocol '$($Protocol)' and address '$($Address)' or local address '$($_localAddress)' does not exist" + throw ($PodeLocale.endpointNotExistExceptionMessage -f $Protocol, $Address, $_localAddress) #"Endpoint with protocol '$($Protocol)' and address '$($Address)' or local address '$($_localAddress)' does not exist" } return $null @@ -310,7 +381,7 @@ function Get-PodeEndpointByName { # error? if ($ThrowError) { - throw "Endpoint with name '$($Name)' does not exist" + throw ($PodeLocale.endpointNameNotExistExceptionMessage -f $Name) #"Endpoint with name '$($Name)' does not exist" } return $null diff --git a/src/Private/FileMonitor.ps1 b/src/Private/FileMonitor.ps1 index deee38e46..7b9fae009 100644 --- a/src/Private/FileMonitor.ps1 +++ b/src/Private/FileMonitor.ps1 @@ -9,15 +9,13 @@ function Start-PodeFileMonitor { $filter = '*.*' # setup the file monitor - $watcher = New-Object System.IO.FileSystemWatcher $folder, $filter -Property @{ - IncludeSubdirectories = $true - NotifyFilter = [System.IO.NotifyFilters]'FileName,LastWrite,CreationTime' - } - + $watcher = [System.IO.FileSystemWatcher]::new($folder, $filter) + $watcher.IncludeSubdirectories = $true + $watcher.NotifyFilter = [System.IO.NotifyFilters]'FileName,LastWrite,CreationTime' $watcher.EnableRaisingEvents = $true # setup the monitor timer - only restart server after changes + 2s of no changes - $timer = New-Object System.Timers.Timer + $timer = [System.Timers.Timer]::new() $timer.AutoReset = $false $timer.Interval = 2000 @@ -68,10 +66,11 @@ function Start-PodeFileMonitor { # if enabled, show the files that triggered the restart if ($Event.MessageData.FileSettings.ShowFiles) { if (!$Event.MessageData.Quiet) { - Write-Host 'The following files have changed:' -ForegroundColor Magenta + # The following files have changed + Write-PodeHost $PodeLocale.filesHaveChangedMessage -ForegroundColor Magenta foreach ($file in $Event.MessageData.FileSettings.Files) { - Write-Host "> $($file)" -ForegroundColor Magenta + Write-PodeHost "> $($file)" -ForegroundColor Magenta } } diff --git a/src/Private/FileWatchers.ps1 b/src/Private/FileWatchers.ps1 index b768ef5a7..ce0ebc4c7 100644 --- a/src/Private/FileWatchers.ps1 +++ b/src/Private/FileWatchers.ps1 @@ -1,13 +1,19 @@ using namespace Pode function Test-PodeFileWatchersExist { + [CmdletBinding()] + [OutputType([bool])] + param() return (($null -ne $PodeContext.Fim) -and (($PodeContext.Fim.Enabled) -or ($PodeContext.Fim.Items.Count -gt 0))) } function New-PodeFileWatcher { + [CmdletBinding()] + [OutputType([PodeWatcher])] + param() $watcher = [PodeWatcher]::new($PodeContext.Tokens.Cancellation.Token) $watcher.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled) - $watcher.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevels) + $watcher.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevel) return $watcher } @@ -103,6 +109,7 @@ function Start-PodeFileWatcherRunspace { $null = Invoke-PodeScriptBlock -ScriptBlock $fileWatcher.Script -Arguments $fileWatcher.Arguments -UsingVariables $fileWatcher.UsingVariables -Scoped -Splat } catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug } catch { $_ | Write-PodeErrorLog @@ -116,6 +123,7 @@ function Start-PodeFileWatcherRunspace { } } catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug } catch { $_ | Write-PodeErrorLog @@ -125,7 +133,7 @@ function Start-PodeFileWatcherRunspace { } 1..$PodeContext.Threads.Files | ForEach-Object { - Add-PodeRunspace -Type Files -ScriptBlock $watchScript -Parameters @{ 'Watcher' = $watcher; 'ThreadId' = $_ } + Add-PodeRunspace -Type Files -Name 'Watcher' -Id $_ -ScriptBlock $watchScript -Parameters @{ 'Watcher' = $watcher ; 'ThreadId' = $_ } } # script to keep file watcher server alive until cancelled @@ -140,7 +148,9 @@ function Start-PodeFileWatcherRunspace { Start-Sleep -Seconds 1 } } - catch [System.OperationCanceledException] {} + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } catch { $_ | Write-PodeErrorLog $_.Exception | Write-PodeErrorLog -CheckInnerException @@ -151,5 +161,5 @@ function Start-PodeFileWatcherRunspace { } } - Add-PodeRunspace -Type Files -ScriptBlock $waitScript -Parameters @{ 'Watcher' = $watcher } -NoProfile + Add-PodeRunspace -Type Files -Name 'KeepAlive' -ScriptBlock $waitScript -Parameters @{ 'Watcher' = $watcher } -NoProfile } diff --git a/src/Private/Gui.ps1 b/src/Private/Gui.ps1 index ec5592abf..94b772795 100644 --- a/src/Private/Gui.ps1 +++ b/src/Private/Gui.ps1 @@ -16,7 +16,8 @@ function Start-PodeGuiRunspace { # if there are multiple endpoints, flag warning we're only using the first - unless explicitly set if ($null -eq $PodeContext.Server.Gui.Endpoint) { if ($PodeContext.Server.Endpoints.Values.Count -gt 1) { - Write-PodeHost 'Multiple endpoints defined, only the first will be used for the GUI' -ForegroundColor Yellow + # Multiple endpoints defined, only the first will be used for the GUI + Write-PodeHost $PodeLocale.multipleEndpointsForGuiMessage -ForegroundColor Yellow } } @@ -41,7 +42,7 @@ function Start-PodeGuiRunspace { Start-Sleep -Milliseconds 200 } else { - throw "Failed to connect to URL: $($uri)" + throw ($PodeLocale.failedToConnectToUrlExceptionMessage -f $uri) #"Failed to connect to URL: $($uri)" } } } @@ -120,7 +121,8 @@ function Start-PodeGuiRunspace { } # display the form - Write-PodeHost 'Opening GUI' -ForegroundColor Yellow + # Opening the GUI + Write-PodeHost $PodeLocale.openingGuiMessage -ForegroundColor Yellow $null = $form.ShowDialog() Start-Sleep -Seconds 1 } @@ -134,5 +136,5 @@ function Start-PodeGuiRunspace { } } - Add-PodeRunspace -Type Gui -ScriptBlock $script + Add-PodeRunspace -Type Gui -Name 'Watcher' -ScriptBlock $script } \ No newline at end of file diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index d16ea4f92..0bdc9237b 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -1,6 +1,32 @@ using namespace Pode -# read in the content from a dynamic pode file and invoke its content +<# +.SYNOPSIS + Dynamically executes content as a Pode file, optionally passing data to it. + +.DESCRIPTION + This function takes a string of content, which is expected to be PowerShell code, and optionally a hashtable of data. It constructs a script block that optionally includes a parameter declaration, + and then executes this script block using the provided data. This is useful for dynamically generating content based on a template or script contained in a file or a string. + +.PARAMETER Content + The PowerShell code as a string. This content is dynamically executed as a script block. It can include placeholders or logic that utilizes the passed data. + +.PARAMETER Data + Optional hashtable of data that can be referenced within the content/script. This data is passed to the script block as parameters. + +.EXAMPLE + $scriptContent = '"Hello, world! Today is $(Get-Date)"' + ConvertFrom-PodeFile -Content $scriptContent + + This example will execute the content of the script and output "Hello, world! Today is [current date]". + +.EXAMPLE + $template = '"Hello, $(Name)! Your balance is $$(Amount)"' + $data = @{ Name = 'John Doe'; Amount = '100.50' } + ConvertFrom-PodeFile -Content $template -Data $data + + This example demonstrates using the function with a data parameter to replace placeholders within the content. +#> function ConvertFrom-PodeFile { param( [Parameter(Mandatory = $true)] @@ -126,7 +152,7 @@ function Test-PodeIsAdminUser { } try { - $principal = New-Object System.Security.Principal.WindowsPrincipal([System.Security.Principal.WindowsIdentity]::GetCurrent()) + $principal = [System.Security.Principal.WindowsPrincipal]::new([System.Security.Principal.WindowsIdentity]::GetCurrent()) if ($null -eq $principal) { return $false } @@ -190,7 +216,7 @@ function Get-PodeEndpointInfo { # validate that we have a valid ip/host:port address if (!(($Address -imatch "^$($cmbdRgx)$") -or ($Address -imatch "^$($hostRgx)[\:]{0,1}") -or ($Address -imatch "[\:]{0,1}$($portRgx)$"))) { - throw "Failed to parse '$($Address)' as a valid IP/Host:Port address" + throw ($PodeLocale.failedToParseAddressExceptionMessage -f $Address)#"Failed to parse '$($Address)' as a valid IP/Host:Port address" } # grab the ip address/hostname @@ -201,7 +227,7 @@ function Get-PodeEndpointInfo { # ensure we have a valid ip address/hostname if (!(Test-PodeIPAddress -IP $_host)) { - throw "The IP address supplied is invalid: $($_host)" + throw ($PodeLocale.invalidIpAddressExceptionMessage -f $_host) #"The IP address supplied is invalid: $($_host)" } # grab the port @@ -212,7 +238,7 @@ function Get-PodeEndpointInfo { # ensure the port is valid if ($_port -lt 0) { - throw "The port cannot be negative: $($_port)" + throw ($PodeLocale.invalidPortExceptionMessage -f $_port)#"The port cannot be negative: $($_port)" } # return the info @@ -525,229 +551,6 @@ function Get-PodeSubnetRange { } } -function Add-PodeRunspace { - param( - [Parameter(Mandatory = $true)] - [ValidateSet('Main', 'Signals', 'Schedules', 'Gui', 'Web', 'Smtp', 'Tcp', 'Tasks', 'WebSockets', 'Files')] - [string] - $Type, - - [Parameter(Mandatory = $true)] - [ValidateNotNull()] - [scriptblock] - $ScriptBlock, - - [Parameter()] - $Parameters, - - [Parameter()] - [System.Management.Automation.PSDataCollection[psobject]] - $OutputStream = $null, - - [switch] - $Forget, - - [switch] - $NoProfile, - - [switch] - $PassThru - ) - - try { - # create powershell pipelines - $ps = [powershell]::Create() - $ps.RunspacePool = $PodeContext.RunspacePools[$Type].Pool - - # load modules/drives - if (!$NoProfile) { - $null = $ps.AddScript("Open-PodeRunspace -Type '$($Type)'") - } - - # load main script - $null = $ps.AddScript($ScriptBlock) - - # load parameters - if (!(Test-PodeIsEmpty $Parameters)) { - $Parameters.Keys | ForEach-Object { - $null = $ps.AddParameter($_, $Parameters[$_]) - } - } - - # start the pipeline - if ($null -eq $OutputStream) { - $pipeline = $ps.BeginInvoke() - } - else { - $pipeline = $ps.BeginInvoke($OutputStream, $OutputStream) - } - - # do we need to remember this pipeline? sorry, what did you say? - if ($Forget) { - $null = $pipeline - } - - # or do we need to return it for custom processing? ie: tasks - elseif ($PassThru) { - return @{ - Pipeline = $ps - Handler = $pipeline - } - } - - # or store it here for later clean-up - else { - $PodeContext.Runspaces += @{ - Pool = $Type - Pipeline = $ps - Handler = $pipeline - Stopped = $false - } - } - } - catch { - $_ | Write-PodeErrorLog - throw $_.Exception - } -} - -function Open-PodeRunspace { - param( - [Parameter(Mandatory = $true)] - [string] - $Type - ) - - try { - Import-PodeModules - Add-PodePSDrives - $PodeContext.RunspacePools[$Type].State = 'Ready' - } - catch { - if ($PodeContext.RunspacePools[$Type].State -ieq 'waiting') { - $PodeContext.RunspacePools[$Type].State = 'Error' - } - - $_ | Out-Default - $_.ScriptStackTrace | Out-Default - throw - } -} - -function Close-PodeRunspaces { - param( - [switch] - $ClosePool - ) - - if ($PodeContext.Server.IsServerless) { - return - } - - try { - if (!(Test-PodeIsEmpty $PodeContext.Runspaces)) { - Write-Verbose 'Waiting until all Listeners are disposed' - - $count = 0 - $continue = $false - while ($count -le 10) { - Start-Sleep -Seconds 1 - $count++ - - $continue = $false - foreach ($listener in $PodeContext.Listeners) { - if (!$listener.IsDisposed) { - $continue = $true - break - } - } - - foreach ($receiver in $PodeContext.Receivers) { - if (!$receiver.IsDisposed) { - $continue = $true - break - } - } - - foreach ($watcher in $PodeContext.Watchers) { - if (!$watcher.IsDisposed) { - $continue = $true - break - } - } - - if ($continue) { - continue - } - - break - } - - Write-Verbose 'All Listeners disposed' - - # now dispose runspaces - Write-Verbose 'Disposing Runspaces' - $runspaceErrors = @(foreach ($item in $PodeContext.Runspaces) { - if ($item.Stopped) { - continue - } - - try { - # only do this, if the pool is in error - if ($PodeContext.RunspacePools[$item.Pool].State -ieq 'error') { - $item.Pipeline.EndInvoke($item.Handler) - } - } - catch { - "$($item.Pool) runspace failed to load: $($_.Exception.InnerException.Message)" - } - - Close-PodeDisposable -Disposable $item.Pipeline - $item.Stopped = $true - }) - - # dispose of schedule runspaces - if ($PodeContext.Schedules.Processes.Count -gt 0) { - foreach ($key in $PodeContext.Schedules.Processes.Keys.Clone()) { - Close-PodeScheduleInternal -Process $PodeContext.Schedules.Processes[$key] - } - } - - # dispose of task runspaces - if ($PodeContext.Tasks.Results.Count -gt 0) { - foreach ($key in $PodeContext.Tasks.Results.Keys.Clone()) { - Close-PodeTaskInternal -Result $PodeContext.Tasks.Results[$key] - } - } - - $PodeContext.Runspaces = @() - Write-Verbose 'Runspaces disposed' - } - - # close/dispose the runspace pools - if ($ClosePool) { - Close-PodeRunspacePools - } - - # check for runspace errors - if (($null -ne $runspaceErrors) -and ($runspaceErrors.Length -gt 0)) { - foreach ($err in $runspaceErrors) { - if ($null -eq $err) { - continue - } - - throw $err - } - } - - # garbage collect - [GC]::Collect() - } - catch { - $_ | Write-PodeErrorLog - throw $_.Exception - } -} function Get-PodeConsoleKey { if ([Console]::IsInputRedirected -or ![Console]::KeyAvailable) { @@ -820,7 +623,7 @@ function Close-PodeServerInternal { # stop all current runspaces Write-Verbose 'Closing runspaces' - Close-PodeRunspaces -ClosePool + Close-PodeRunspace -ClosePool # stop the file monitor if it's running Write-Verbose 'Stopping file monitor' @@ -843,10 +646,10 @@ function Close-PodeServerInternal { # remove all of the pode temp drives Write-Verbose 'Removing internal PSDrives' - Remove-PodePSDrives + Remove-PodePSDrive if ($ShowDoneMessage -and ($PodeContext.Server.Types.Length -gt 0) -and !$PodeContext.Server.IsServerless) { - Write-PodeHost ' Done' -ForegroundColor Green + Write-PodeHost $PodeLocale.doneMessage -ForegroundColor Green } } @@ -873,7 +676,7 @@ function New-PodePSDrive { # if the path supplied doesn't exist, error if (!(Test-Path $Path)) { - throw "Path does not exist: $($Path)" + throw ($PodeLocale.pathNotExistExceptionMessage -f $Path)#"Path does not exist: $($Path)" } # resolve the path @@ -928,13 +731,42 @@ function Test-PodePSDrive { return $true } -function Add-PodePSDrives { +<# +.SYNOPSIS + Adds Pode PS drives to the session. + +.DESCRIPTION + This function iterates through the keys of Pode drives stored in the `$PodeContext.Server.Drives` collection and creates corresponding PS drives using `New-PodePSDrive`. The drive paths are specified by the values associated with each key. + +.EXAMPLE + Add-PodePSDrivesInternal + # Creates Pode PS drives in the session based on the configured drive paths. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Add-PodePSDrivesInternal { foreach ($key in $PodeContext.Server.Drives.Keys) { $null = New-PodePSDrive -Path $PodeContext.Server.Drives[$key] -Name $key } } -function Import-PodeModules { +<# +.SYNOPSIS + Imports other Pode modules into the session. + +.DESCRIPTION + This function iterates through the paths of other Pode modules stored in the `$PodeContext.Server.Modules.Values` collection and imports them into the session. + It uses the `-DisableNameChecking` switch to suppress name checking during module import. + +.EXAMPLE + Import-PodeModulesInternal + # Imports other Pode modules into the session. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Import-PodeModulesInternal { # import other modules in the session foreach ($path in $PodeContext.Server.Modules.Values) { if (Test-Path $path) { @@ -960,9 +792,7 @@ Add-PodePSInbuiltDrive This example is typically called within the Pode server setup script or internally by the Pode framework to initialize the PowerShell drives for the server's default folders. .NOTES -- The function is designed to be used within the Pode framework and relies on the global `$PodeContext` variable for configuration. -- It specifically checks for the existence of paths for views, public content, and errors before attempting to create drives for them. -- This is an internal function and may change in future releases of Pode. +This is an internal function and may change in future releases of Pode. #> function Add-PodePSInbuiltDrive { @@ -985,11 +815,66 @@ function Add-PodePSInbuiltDrive { } } -function Remove-PodePSDrives { - $null = Get-PSDrive PodeDir* | Remove-PSDrive +<# +.SYNOPSIS + Removes Pode PS drives from the session. + +.DESCRIPTION + This function removes Pode PS drives from the session based on the specified drive name or pattern. + If no specific name or pattern is provided, it removes all Pode PS drives by default. + It uses `Get-PSDrive` to retrieve the drives and `Remove-PSDrive` to remove them. + +.PARAMETER Name + The name or pattern of the Pode PS drives to remove. Defaults to 'PodeDir*'. + +.EXAMPLE + Remove-PodePSDrive -Name 'myDir*' + # Removes all PS drives with names matching the pattern 'myDir*'. + +.EXAMPLE + Remove-PodePSDrive + # Removes all Pode PS drives. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Remove-PodePSDrive { + [CmdletBinding()] + param( + $Name = 'PodeDir*' + ) + $null = Get-PSDrive -Name $Name | Remove-PSDrive } +<# +.SYNOPSIS + Joins a folder and file path to the root path of the server. + +.DESCRIPTION + This function combines a folder path, file path (optional), and the root path of the server to create a complete path. If the root path is not explicitly provided, it uses the default root path from the Pode context. + +.PARAMETER Folder + The folder path to join. + +.PARAMETER FilePath + The file path (optional) to join. If not provided, only the folder path is used. + +.PARAMETER Root + The root path of the server. If not provided, the default root path from the Pode context is used. + +.OUTPUTS + Returns the combined path as a string. + +.EXAMPLE + Join-PodeServerRoot -Folder "uploads" -FilePath "document.txt" + # Output: "/uploads/document.txt" + + This example combines the folder path "uploads" and the file path "document.txt" with the default root path from the Pode context. + +#> function Join-PodeServerRoot { + [CmdletBinding()] + [OutputType([string])] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] @@ -1014,60 +899,70 @@ function Join-PodeServerRoot { return [System.IO.Path]::Combine($Root, $Folder, $FilePath) } +<# +.SYNOPSIS + Removes empty items (empty strings) from an array. + +.DESCRIPTION + This function filters out empty items (empty strings) from an array. It returns a new array containing only non-empty items. + +.PARAMETER Array + The array from which to remove empty items. + +.OUTPUTS + Returns an array containing non-empty items. + +.EXAMPLE + $myArray = "apple", "", "banana", "", "cherry" + $filteredArray = Remove-PodeEmptyItemsFromArray -Array $myArray + Write-PodeHost "Filtered array: $filteredArray" + + This example removes empty items from the array and displays the filtered array. +#> function Remove-PodeEmptyItemsFromArray { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSPossibleIncorrectComparisonWithNull', '')] + [CmdletBinding()] + [OutputType([System.Object[]])] param( - [Parameter(ValueFromPipeline = $true)] + [Parameter()] $Array ) - if ($null -eq $Array) { return @() } return @( @($Array -ne ([string]::Empty)) -ne $null ) + } -function Remove-PodeNullKeysFromHashtable { - param( - [Parameter(ValueFromPipeline = $true)] - [hashtable] - $Hashtable - ) +<# +.SYNOPSIS + Retrieves the file extension from a given path. - foreach ($key in ($Hashtable.Clone()).Keys) { - if ($null -eq $Hashtable[$key]) { - $null = $Hashtable.Remove($key) - continue - } +.DESCRIPTION + This function extracts the file extension (including the period) from a specified path. Optionally, it can trim the period from the extension. - if (($Hashtable[$key] -is [string]) -and [string]::IsNullOrEmpty($Hashtable[$key])) { - $null = $Hashtable.Remove($key) - continue - } +.PARAMETER Path + The path from which to extract the file extension. - if ($Hashtable[$key] -is [array]) { - if (($Hashtable[$key].Length -eq 1) -and ($null -eq $Hashtable[$key][0])) { - $null = $Hashtable.Remove($key) - continue - } +.PARAMETER TrimPeriod + Switch parameter. If specified, trims the period from the file extension. - foreach ($item in $Hashtable[$key]) { - if (($item -is [hashtable]) -or ($item -is [System.Collections.Specialized.OrderedDictionary])) { - $item | Remove-PodeNullKeysFromHashtable - } - } +.OUTPUTS + Returns the file extension (with or without the period) as a string. - continue - } +.EXAMPLE + Get-PodeFileExtension -Path "C:\MyFiles\document.txt" + # Output: ".txt" - if (($Hashtable[$key] -is [hashtable]) -or ($Hashtable[$key] -is [System.Collections.Specialized.OrderedDictionary])) { - $Hashtable[$key] | Remove-PodeNullKeysFromHashtable - continue - } - } -} + Get-PodeFileExtension -Path "C:\MyFiles\document.txt" -TrimPeriod + # Output: "txt" + This example demonstrates how to retrieve the file extension with and without the period from a given path. +#> function Get-PodeFileExtension { + [CmdletBinding()] + [OutputType([string])] param( [Parameter()] [string] @@ -1077,7 +972,10 @@ function Get-PodeFileExtension { $TrimPeriod ) + # Get the file extension $ext = [System.IO.Path]::GetExtension($Path) + + # Trim the period if requested if ($TrimPeriod) { $ext = $ext.Trim('.') } @@ -1085,7 +983,39 @@ function Get-PodeFileExtension { return $ext } + +<# +.SYNOPSIS + Retrieves the file name from a given path. + +.DESCRIPTION + This function extracts the file name (including the extension) or the file name without the extension from a specified path. + +.PARAMETER Path + The path from which to extract the file name. + +.PARAMETER WithoutExtension + Switch parameter. If specified, returns the file name without the extension. + +.OUTPUTS + Returns the file name (with or without extension) as a string. + +.EXAMPLE + Get-PodeFileName -Path "C:\MyFiles\document.txt" + # Output: "document.txt" + + Get-PodeFileName -Path "C:\MyFiles\document.txt" -WithoutExtension + # Output: "document" + + This example demonstrates how to retrieve the file name with and without the extension from a given path. + +.NOTES + - If the path is a directory, the function returns the directory name. + - Use this function to extract file names for further processing or display. +#> function Get-PodeFileName { + [CmdletBinding()] + [OutputType([string])] param( [Parameter()] [string] @@ -1102,7 +1032,29 @@ function Get-PodeFileName { return [System.IO.Path]::GetFileName($Path) } +<# +.SYNOPSIS + Tests whether an exception message indicates a valid network failure. + +.DESCRIPTION + This function checks if an exception message contains specific phrases that commonly indicate network-related failures. It returns a boolean value indicating whether the exception message matches any of these network failure patterns. + +.PARAMETER Exception + The exception object whose message needs to be tested. + +.OUTPUTS + Returns $true if the exception message indicates a valid network failure, otherwise returns $false. + +.EXAMPLE + $exception = [System.Exception]::new("The network name is no longer available.") + $isNetworkFailure = Test-PodeValidNetworkFailure -Exception $exception + Write-PodeHost "Is network failure: $isNetworkFailure" + + This example tests whether the exception message "The network name is no longer available." indicates a network failure. +#> function Test-PodeValidNetworkFailure { + [CmdletBinding()] + [OutputType([bool])] param( [Parameter()] $Exception @@ -1126,35 +1078,37 @@ function Test-PodeValidNetworkFailure { function ConvertFrom-PodeHeaderQValue { param( - [Parameter(ValueFromPipeline = $true)] + [Parameter()] [string] $Value ) - $qs = [ordered]@{} - - # return if no value - if ([string]::IsNullOrWhiteSpace($Value)) { - return $qs - } - - # split the values up - $parts = @($Value -isplit ',').Trim() + process { + $qs = [ordered]@{} - # go through each part and check its q-value - foreach ($part in $parts) { - # default of 1 if no q-value - if ($part.IndexOf(';q=') -eq -1) { - $qs[$part] = 1.0 - continue + # return if no value + if ([string]::IsNullOrWhiteSpace($Value)) { + return $qs } - # parse for q-value - $atoms = @($part -isplit ';q=') - $qs[$atoms[0]] = [double]$atoms[1] - } + # split the values up + $parts = @($Value -isplit ',').Trim() - return $qs + # go through each part and check its q-value + foreach ($part in $parts) { + # default of 1 if no q-value + if ($part.IndexOf(';q=') -eq -1) { + $qs[$part] = 1.0 + continue + } + + # parse for q-value + $atoms = @($part -isplit ';q=') + $qs[$atoms[0]] = [double]$atoms[1] + } + + return $qs + } } function Get-PodeAcceptEncoding { @@ -1243,7 +1197,42 @@ function Get-PodeAcceptEncoding { return $found.Name } -function Get-PodeRanges { +<# +.SYNOPSIS + Parses a range string and converts it into a hashtable array of start and end values. + +.DESCRIPTION + This function takes a range string (typically used in HTTP headers) and extracts the relevant start and end values. It supports the 'bytes' unit and handles multiple ranges separated by commas. + +.PARAMETER Range + The range string to parse. + +.PARAMETER ThrowError + A switch parameter. If specified, the function throws an exception (HTTP status code 416) when encountering invalid range formats. + +.OUTPUTS + An array of hashtables, each containing 'Start' and 'End' properties representing the parsed ranges. + +.EXAMPLE + Get-PodeRange -Range 'bytes=100-200,300-400' + # Returns an array of hashtables: + # [ + # @{ + # Start = 100 + # End = 200 + # }, + # @{ + # Start = 300 + # End = 400 + # } + # ] + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Get-PodeRange { + [CmdletBinding()] + [OutputType([hashtable[]])] param( [Parameter()] [string] @@ -1388,7 +1377,7 @@ function New-PodeRequestException { function ConvertTo-PodeResponseContent { param( - [Parameter(ValueFromPipeline = $true)] + [Parameter()] $InputObject, [Parameter()] @@ -1406,7 +1395,6 @@ function ConvertTo-PodeResponseContent { [switch] $AsHtml ) - # split for the main content type $ContentType = Split-PodeContentType -ContentType $ContentType @@ -1417,7 +1405,7 @@ function ConvertTo-PodeResponseContent { # run action for the content type switch ($ContentType) { - { $_ -ilike '*/json' } { + { $_ -match '^(.*\/)?(.*\+)?json$' } { if ($InputObject -isnot [string]) { if ($Depth -le 0) { return (ConvertTo-Json -InputObject $InputObject -Compress) @@ -1432,7 +1420,7 @@ function ConvertTo-PodeResponseContent { } } - { $_ -ilike '*/yaml' -or $_ -ilike '*/x-yaml' } { + { $_ -match '^(.*\/)?(.*\+)?yaml$' } { if ($InputObject -isnot [string]) { if ($Depth -le 0) { return (ConvertTo-PodeYamlInternal -InputObject $InputObject ) @@ -1447,10 +1435,10 @@ function ConvertTo-PodeResponseContent { } } - { $_ -ilike '*/xml' } { + { $_ -match '^(.*\/)?(.*\+)?xml$' } { if ($InputObject -isnot [string]) { $temp = @(foreach ($item in $InputObject) { - New-Object psobject -Property $item + [pscustomobject]$item }) return ($temp | ConvertTo-Xml -Depth $Depth -As String -NoTypeInformation) @@ -1464,7 +1452,7 @@ function ConvertTo-PodeResponseContent { { $_ -ilike '*/csv' } { if ($InputObject -isnot [string]) { $temp = @(foreach ($item in $InputObject) { - New-Object psobject -Property $item + [pscustomobject]$item }) if (Test-PodeIsPSCore) { @@ -1548,10 +1536,10 @@ function ConvertFrom-PodeRequestContent { # if the request is compressed, attempt to uncompress it if (![string]::IsNullOrWhiteSpace($TransferEncoding)) { # create a compressed stream to decompress the req bytes - $ms = New-Object -TypeName System.IO.MemoryStream + $ms = [System.IO.MemoryStream]::new() $ms.Write($Request.RawBody, 0, $Request.RawBody.Length) $null = $ms.Seek(0, 0) - $stream = New-Object "System.IO.Compression.$($TransferEncoding)Stream"($ms, [System.IO.Compression.CompressionMode]::Decompress) + $stream = Get-PodeCompressionStream -InputStream $ms -Encoding $TransferEncoding -Mode Decompress # read the decompressed bytes $Content = Read-PodeStreamToEnd -Stream $stream -Encoding $Request.ContentEncoding @@ -1645,7 +1633,27 @@ function ConvertFrom-PodeRequestContent { $Content = $null return $Result } +<# +.SYNOPSIS + Extracts the base MIME type from a Content-Type string that may include additional parameters. +.DESCRIPTION + This function takes a Content-Type string as input and returns only the base MIME type by splitting the string at the semicolon (';') and trimming any excess whitespace. + It is useful for handling HTTP headers or other contexts where Content-Type strings include parameters like charset, boundary, etc. + +.PARAMETER ContentType + The Content-Type string from which to extract the base MIME type. This string can include additional parameters separated by semicolons. + +.EXAMPLE + Split-PodeContentType -ContentType "text/html; charset=UTF-8" + + This example returns 'text/html', stripping away the 'charset=UTF-8' parameter. + +.EXAMPLE + Split-PodeContentType -ContentType "application/json; charset=utf-8" + + This example returns 'application/json', removing the charset parameter. +#> function Split-PodeContentType { param( [Parameter()] @@ -1653,10 +1661,13 @@ function Split-PodeContentType { $ContentType ) + # Check if the input string is null, empty, or consists only of whitespace. if ([string]::IsNullOrWhiteSpace($ContentType)) { - return [string]::Empty + return [string]::Empty # Return an empty string if the input is not valid. } + # Split the Content-Type string by the semicolon, which separates the base MIME type from other parameters. + # Trim any leading or trailing whitespace from the resulting MIME type to ensure clean output. return @($ContentType -isplit ';')[0].Trim() } @@ -1684,27 +1695,68 @@ function ConvertFrom-PodeNameValueToHashTable { return $ht } +<# +.SYNOPSIS + Gets the count of elements in the provided object or the length of a string. + +.DESCRIPTION + This function returns the count of elements in various types of objects including strings, collections, and arrays. + If the object is a string, it returns the length of the string. If the object is null or an empty collection, it returns 0. + This function is useful for determining the size or length of data containers in PowerShell scripts. + +.PARAMETER Object + The object from which the count or length will be determined. This can be a string, array, collection, or any other object that has a Count property. + +.OUTPUTS + [int] + Returns an integer representing the count of elements or length of the string. + +.EXAMPLE + $array = @(1, 2, 3) + Get-PodeCount -Object $array + + This example returns 3, as there are three elements in the array. + +.EXAMPLE + $string = "hello" + Get-PodeCount -Object $string + + This example returns 5, as there are five characters in the string. + +.EXAMPLE + $nullObject = $null + Get-PodeCount -Object $nullObject + + This example returns 0, as the object is null. +#> function Get-PodeCount { + [CmdletBinding()] + [OutputType([int])] param( [Parameter()] - $Object + $Object # The object to be evaluated for its count. ) + # Check if the object is null. if ($null -eq $Object) { - return 0 + return 0 # Return 0 if the object is null. } + # Check if the object is a string and return its length. if ($Object -is [string]) { return $Object.Length } + # Check if the object is a NameValueCollection and is empty. if ($Object -is [System.Collections.Specialized.NameValueCollection] -and $Object.Count -eq 0) { - return 0 + return 0 # Return 0 if the collection is empty. } + # For other types of collections, return their Count property. return $Object.Count } + <# .SYNOPSIS Tests if a given file system path is valid and optionally if it is not a directory. @@ -1743,7 +1795,6 @@ function Get-PodeCount { This function is used within the Pode framework to validate file system paths for serving static content. #> - function Test-PodePath { param( [Parameter()] @@ -1855,18 +1906,7 @@ function Test-PodePathIsDirectory { return ([string]::IsNullOrWhiteSpace([System.IO.Path]::GetExtension($Path))) } -function Convert-PodePathSeparators { - param( - [Parameter()] - $Paths - ) - return @($Paths | ForEach-Object { - if (![string]::IsNullOrWhiteSpace($_)) { - $_ -ireplace '[\\/]', [System.IO.Path]::DirectorySeparatorChar - } - }) -} function Convert-PodePathPatternToRegex { param( @@ -1934,57 +1974,144 @@ function Convert-PodePathPatternsToRegex { return "^$($joined)$" } -function Get-PodeDefaultSslProtocols { +<# +.SYNOPSIS + Gets the default SSL protocol(s) based on the operating system. + +.DESCRIPTION + This function determines the appropriate default SSL protocol(s) based on the operating system. On macOS, it returns TLS 1.2. On other platforms, it combines SSL 3.0 and TLS 1.2. + +.OUTPUTS + A [System.Security.Authentication.SslProtocols] enum value representing the default SSL protocol(s). + +.EXAMPLE + Get-PodeDefaultSslProtocol + # Returns [System.Security.Authentication.SslProtocols]::Ssl3, [System.Security.Authentication.SslProtocols]::Tls12 (on non-macOS systems) + # Returns [System.Security.Authentication.SslProtocols]::Tls12 (on macOS) + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Get-PodeDefaultSslProtocol { + [CmdletBinding()] + [OutputType([System.Security.Authentication.SslProtocols])] + param() if (Test-PodeIsMacOS) { - return (ConvertTo-PodeSslProtocols -Protocols Tls12) + return (ConvertTo-PodeSslProtocol -Protocol Tls12) } - return (ConvertTo-PodeSslProtocols -Protocols Ssl3, Tls12) + return (ConvertTo-PodeSslProtocol -Protocol Ssl3, Tls12) } -function ConvertTo-PodeSslProtocols { +<# +.SYNOPSIS + Converts a string representation of SSL protocols to the corresponding SslProtocols enum value. + +.DESCRIPTION + This function takes an array of SSL protocol strings (such as 'Tls', 'Tls12', etc.) and combines them into a single SslProtocols enum value. It's useful for configuring SSL/TLS settings in Pode or other PowerShell scripts. + +.PARAMETER Protocol + An array of SSL protocol strings. Valid values are 'Ssl2', 'Ssl3', 'Tls', 'Tls11', 'Tls12', and 'Tls13'. + +.OUTPUTS + A [System.Security.Authentication.SslProtocols] enum value representing the combined protocols. + +.EXAMPLE + ConvertTo-PodeSslProtocol -Protocol 'Tls', 'Tls12' + # Returns [System.Security.Authentication.SslProtocols]::Tls12 + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function ConvertTo-PodeSslProtocol { + [CmdletBinding()] + [OutputType([System.Security.Authentication.SslProtocols])] param( [Parameter()] [ValidateSet('Ssl2', 'Ssl3', 'Tls', 'Tls11', 'Tls12', 'Tls13')] [string[]] - $Protocols + $Protocol ) $protos = 0 - foreach ($protocol in $Protocols) { - $protos = [int]($protos -bor [System.Security.Authentication.SslProtocols]::$protocol) + foreach ($item in $Protocol) { + $protos = [int]($protos -bor [System.Security.Authentication.SslProtocols]::$item) } return [System.Security.Authentication.SslProtocols]($protos) } -function Get-PodeModuleDetails { +<# +.SYNOPSIS + Retrieves details about the Pode module. + +.DESCRIPTION + This function determines the relevant details of the Pode module. It first checks if the module is already imported. + If so, it uses that module. Otherwise, it attempts to identify the module used for the 'engine' and retrieves its details. + If there are multiple versions of the module, it selects the newest version. If no module is imported, it uses the latest installed version. + +.OUTPUTS + A hashtable containing the module details. + +.EXAMPLE + Get-PodeModuleInfo + # Returns a hashtable with module details such as name, path, base path, data path, internal path, and whether it's in the system path. + + .NOTES + This is an internal function and may change in future releases of Pode. +#> +function Get-PodeModuleInfo { + [CmdletBinding()] + [OutputType([hashtable])] + param() # if there's 1 module imported already, use that $importedModule = @(Get-Module -Name Pode) if (($importedModule | Measure-Object).Count -eq 1) { - return (Convert-PodeModuleDetails -Module @($importedModule)[0]) + return (Convert-PodeModuleInfo -Module @($importedModule)[0]) } # if there's none or more, attempt to get the module used for 'engine' try { $usedModule = (Get-Command -Name 'Set-PodeViewEngine').Module if (($usedModule | Measure-Object).Count -eq 1) { - return (Convert-PodeModuleDetails -Module $usedModule) + return (Convert-PodeModuleInfo -Module $usedModule) } } catch { + $_ | Write-PodeErrorLog -Level Debug } # if there were multiple to begin with, use the newest version if (($importedModule | Measure-Object).Count -gt 1) { - return (Convert-PodeModuleDetails -Module @($importedModule | Sort-Object -Property Version)[-1]) + return (Convert-PodeModuleInfo -Module @($importedModule | Sort-Object -Property Version)[-1]) } # otherwise there were none, use the latest installed - return (Convert-PodeModuleDetails -Module @(Get-Module -ListAvailable -Name Pode | Sort-Object -Property Version)[-1]) + return (Convert-PodeModuleInfo -Module @(Get-Module -ListAvailable -Name Pode | Sort-Object -Property Version)[-1]) } -function Convert-PodeModuleDetails { +<# +.SYNOPSIS + Converts Pode module details to a hashtable. + +.DESCRIPTION + This function takes a Pode module and extracts relevant details such as name, path, base path, data path, internal path, and whether it's in the system path. + +.PARAMETER Module + The Pode module to convert. + +.OUTPUTS + A hashtable containing the module details. + +.EXAMPLE + Convert-PodeModuleInfo -Module (Get-Module Pode) + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Convert-PodeModuleInfo { + [CmdletBinding()] + [OutputType([hashtable])] param( [Parameter(Mandatory = $true)] [psmoduleinfo] @@ -2004,45 +2131,96 @@ function Convert-PodeModuleDetails { return $details } +<# +.SYNOPSIS + Checks if a PowerShell module is located within the directories specified in the PSModulePath environment variable. + +.DESCRIPTION + This function determines if the path of a provided PowerShell module starts with any path included in the system's PSModulePath environment variable. + This is used to ensure that the module is being loaded from expected locations, which can be important for security and configuration verification. + +.PARAMETER Module + The module to be checked. This should be a module info object, typically obtained via Get-Module or Import-Module. + +.OUTPUTS + [bool] + Returns $true if the module's path is under a path listed in PSModulePath, otherwise returns $false. + +.EXAMPLE + $module = Get-Module -Name Pode + Test-PodeModuleInPath -Module $module + + This example checks if the 'Pode' module is located within the paths specified by the PSModulePath environment variable. +#> function Test-PodeModuleInPath { + [CmdletBinding()] + [OutputType([bool])] param( [Parameter(Mandatory = $true)] [psmoduleinfo] $Module ) - $separator = ';' - if (Test-PodeIsUnix) { - $separator = ':' - } + # Determine the path separator based on the operating system. + $separator = if (Test-PodeIsUnix) { ':' } else { ';' } + # Split the PSModulePath environment variable to get individual paths. $paths = @($env:PSModulePath -split $separator) + # Check each path to see if the module's path starts with it. foreach ($path in $paths) { + # Return true if the module is in one of the paths. if ($Module.Path.StartsWith($path)) { return $true } } + # Return false if no matching path is found. return $false } +<# +.SYNOPSIS + Retrieves a module and all of its recursive dependencies. + +.DESCRIPTION + This function takes a PowerShell module as input and returns an array containing + the module and all of its required dependencies, retrieved recursively. This is + useful for understanding the full set of dependencies a module has. + +.PARAMETER Module + The module for which to retrieve dependencies. This must be a valid PowerShell module object. + +.EXAMPLE + $module = Get-Module -Name SomeModuleName + $dependencies = Get-PodeModuleDependencyList -Module $module + This example retrieves all dependencies for "SomeModuleName". -function Get-PodeModuleDependencies { +.OUTPUTS + Array[psmoduleinfo] + Returns an array of psmoduleinfo objects, each representing a module in the dependency tree. +#> + +function Get-PodeModuleDependencyList { param( [Parameter(Mandatory = $true)] [psmoduleinfo] $Module ) + # Check if the module has any required modules (dependencies). if (!$Module.RequiredModules) { return $Module } - + # Initialize an array to hold all dependencies. $mods = @() + + # Iterate through each required module and recursively retrieve their dependencies. foreach ($mod in $Module.RequiredModules) { - $mods += (Get-PodeModuleDependencies -Module $mod) + # Recursive call for each dependency. + $mods += (Get-PodeModuleDependencyList -Module $mod) } + # Return the list of all dependencies plus the original module. return ($mods + $module) } @@ -2333,13 +2511,43 @@ function Get-PodeRelativePath { # if flagged, test the path and throw error if it doesn't exist if ($TestPath -and !(Test-PodePath $Path -NoStatus)) { - throw "The path does not exist: $(Protect-PodeValue -Value $Path -Default $_rawPath)" + throw ($PodeLocale.pathNotExistExceptionMessage -f (Protect-PodeValue -Value $Path -Default $_rawPath))#"The path does not exist: $(Protect-PodeValue -Value $Path -Default $_rawPath)" } return $Path } -function Get-PodeWildcardFiles { +<# +.SYNOPSIS + Retrieves files based on a wildcard pattern in a given path. + +.DESCRIPTION + The `Get-PodeWildcardFile` function returns files from the specified path based on a wildcard pattern. + You can customize the wildcard and provide an optional root path for relative paths. + +.PARAMETER Path + Specifies the path to search for files. This parameter is mandatory. + +.PARAMETER Wildcard + Specifies the wildcard pattern for file matching. Default is '*.*'. + +.PARAMETER RootPath + Specifies an optional root path for relative paths. If provided, the function will join the root path with the specified path. + +.OUTPUTS + Returns an array of file paths matching the wildcard pattern. + +.EXAMPLE + # Example usage: + $files = Get-PodeWildcardFile -Path '/path/to/files' -Wildcard '*.txt' + # Returns an array of .txt files in the specified path. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Get-PodeWildcardFile { + [CmdletBinding()] + [OutputType([object[]])] param( [Parameter(Mandatory = $true)] [string] @@ -2379,7 +2587,7 @@ function Test-PodeIsServerless { ) if ($PodeContext.Server.IsServerless -and $ThrowError) { - throw "The $($FunctionName) function is not supported in a serverless context" + throw ($PodeLocale.unsupportedFunctionInServerlessContextExceptionMessage -f $FunctionName) #"The $($FunctionName) function is not supported in a serverless context" } if (!$ThrowError) { @@ -2502,24 +2710,25 @@ function Get-PodeHandler { function Convert-PodeFileToScriptBlock { param( [Parameter(Mandatory = $true)] + [Alias('FilePath')] [string] - $FilePath + $Path ) # resolve for relative path - $FilePath = Get-PodeRelativePath -Path $FilePath -JoinRoot + $Path = Get-PodeRelativePath -Path $Path -JoinRoot - # if file doesn't exist, error - if (!(Test-PodePath -Path $FilePath -NoStatus)) { - throw "The FilePath supplied does not exist: $($FilePath)" + # if Path doesn't exist, error + if (!(Test-PodePath -Path $Path -NoStatus)) { + throw ($PodeLocale.pathNotExistExceptionMessage -f $Path) # "The Path supplied does not exist: $($Path)" } # if the path is a wildcard or directory, error - if (!(Test-PodePathIsFile -Path $FilePath -FailOnWildcard)) { - throw "The FilePath supplied cannot be a wildcard or a directory: $($FilePath)" + if (!(Test-PodePathIsFile -Path $Path -FailOnWildcard)) { + throw ($PodeLocale.invalidPathWildcardOrDirectoryExceptionMessage -f $Path) # "The Path supplied cannot be a wildcard or a directory: $($Path)" } - return ([scriptblock](Use-PodeScript -Path $FilePath)) + return ([scriptblock](Use-PodeScript -Path $Path)) } function Convert-PodeQueryStringToHashTable { @@ -2546,66 +2755,19 @@ function Convert-PodeQueryStringToHashTable { return (ConvertFrom-PodeNameValueToHashTable -Collection $tmpQuery) } -function Get-PodeDotSourcedFiles { - param( - [Parameter(Mandatory = $true)] - [System.Management.Automation.Language.Ast] - $Ast, - - [Parameter()] - [string] - $RootPath - ) - - # set default root path - if ([string]::IsNullOrWhiteSpace($RootPath)) { - $RootPath = $PodeContext.Server.Root - } - - # get all dot-sourced files - $cmdTypes = @('dot', 'ampersand') - $files = ($Ast.FindAll({ - ($args[0] -is [System.Management.Automation.Language.CommandAst]) -and - ($args[0].InvocationOperator -iin $cmdTypes) -and - ($args[0].CommandElements.StaticType.Name -ieq 'string') - }, $false)).CommandElements.Value - - $fileOrder = @() - - # no files found - if (($null -eq $files) -or ($files.Length -eq 0)) { - return $fileOrder - } - - # get any sub sourced files - foreach ($file in $files) { - $file = Get-PodeRelativePath -Path $file -RootPath $RootPath -JoinRoot - $fileOrder += $file - - $ast = Get-PodeAstFromFile -FilePath $file - - $result = Get-PodeDotSourcedFiles -Ast $ast -RootPath (Split-Path -Parent -Path $file) - if (($null -ne $result) -and ($result.Length -gt 0)) { - $fileOrder += $result - } - } - - # return all found files - return $fileOrder -} - function Get-PodeAstFromFile { param( [Parameter(Mandatory = $true)] + [Alias('FilePath')] [string] - $FilePath + $Path ) - if (!(Test-Path $FilePath)) { - throw "Path to script file does not exist: $($FilePath)" + if (!(Test-Path $Path)) { + throw ($PodeLocale.pathNotExistExceptionMessage -f $Path) # "The Path supplied does not exist: $($Path)" } - return [System.Management.Automation.Language.Parser]::ParseFile($FilePath, [ref]$null, [ref]$null) + return [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$null, [ref]$null) } function Get-PodeFunctionsFromFile { @@ -2683,7 +2845,34 @@ function Get-PodeFunctionsFromScriptBlock { return $foundFuncs } -function Read-PodeWebExceptionDetails { +<# +.SYNOPSIS + Reads details from a web exception and returns relevant information. + +.DESCRIPTION + The `Read-PodeWebExceptionInfo` function processes a web exception (either `WebException` or `HttpRequestException`) + and extracts relevant details such as status code, status description, and response body. + +.PARAMETER ErrorRecord + Specifies the error record containing the web exception. This parameter is mandatory. + +.OUTPUTS + Returns a hashtable with the following keys: + - `Status`: A nested hashtable with `Code` (status code) and `Description` (status description). + - `Body`: The response body from the web exception. + +.EXAMPLE + # Example usage: + $errorRecord = Get-ErrorRecordFromWebException + $details = Read-PodeWebExceptionInfo -ErrorRecord $errorRecord + # Returns a hashtable with status code, description, and response body. + +.NOTES + This is an internal function and may change in future releases of Pode +#> +function Read-PodeWebExceptionInfo { + [CmdletBinding()] + [OutputType([hashtable])] param( [Parameter(Mandatory = $true)] [System.Management.Automation.ErrorRecord] @@ -2707,7 +2896,8 @@ function Read-PodeWebExceptionDetails { } default { - throw "Exception is of an invalid type, should be either WebException or HttpRequestException, but got: $($_.Exception.GetType().Name)" + #Exception is of an invalid type, should be either WebException or HttpRequestException + throw ($PodeLocale.invalidWebExceptionTypeExceptionMessage -f ($_.Exception.GetType().Name)) } } @@ -2741,7 +2931,7 @@ function Use-PodeFolder { # fail if path not found if (!(Test-PodePath -Path $Path -NoStatus)) { - throw "Path to load $($DefaultPath) not found: $($Path)" + throw ($PodeLocale.pathToLoadNotFoundExceptionMessage -f $DefaultPath, $Path) #"Path to load $($DefaultPath) not found: $($Path)" } # get .ps1 files and load them @@ -2805,9 +2995,30 @@ function Find-PodeModuleFile { return $path } -function Clear-PodeHashtableInnerKeys { +<# +.SYNOPSIS + Clears the inner keys of a hashtable. + +.DESCRIPTION + This function takes a hashtable as input and clears the values associated with each inner key. If the input hashtable is empty or null, no action is taken. + +.PARAMETER InputObject + The hashtable to process. + +.EXAMPLE + $myHashtable = @{ + 'Key1' = 'Value1' + 'Key2' = 'Value2' + } + Clear-PodeHashtableInnerKey -InputObject $myHashtable + # Clears the values associated with 'Key1' and 'Key2' in the hashtable. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Clear-PodeHashtableInnerKey { param( - [Parameter(ValueFromPipeline = $true)] + [Parameter()] [hashtable] $InputObject ) @@ -2845,7 +3056,7 @@ function Set-PodeCronInterval { } if ($Value.Length -gt 1) { - throw "You can only supply a single $($Type) value when using intervals" + throw ($PodeLocale.singleValueForIntervalExceptionMessage -f $Type) #"You can only supply a single $($Type) value when using intervals" } if ($Value.Length -eq 1) { @@ -2870,7 +3081,45 @@ function Get-PodePlaceholderRegex { return '\:(?[\w]+)' } -function Resolve-PodePlaceholders { +<# +.SYNOPSIS + Resolves placeholders in a given path using a specified regex pattern. + +.DESCRIPTION + The `Resolve-PodePlaceholder` function replaces placeholders in the provided path + with custom placeholders based on the specified regex pattern. You can customize + the prepend and append strings for the new placeholders. Additionally, you can + choose to escape slashes in the path. + +.PARAMETER Path + Specifies the path to resolve. This parameter is mandatory. + +.PARAMETER Pattern + Specifies the regex pattern for identifying placeholders. If not provided, the default + placeholder regex pattern from `Get-PodePlaceholderRegex` is used. + +.PARAMETER Prepend + Specifies the string to prepend to the new placeholders. Default is '(?<'. + +.PARAMETER Append + Specifies the string to append to the new placeholders. Default is '>[^\/]+?)'. + +.PARAMETER Slashes + If specified, escapes slashes in the path. + +.OUTPUTS + Returns the resolved path with replaced placeholders. + +.EXAMPLE + # Example usage: + $originalPath = '/api/users/{id}' + $resolvedPath = Resolve-PodePlaceholder -Path $originalPath + # Returns '/api/users/(?[^\/]+?)' with custom placeholders. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Resolve-PodePlaceholder { param( [Parameter(Mandatory = $true)] [string] @@ -2905,10 +3154,46 @@ function Resolve-PodePlaceholders { $Path = "$($Path)[\\\/]" } - return (Convert-PodePlaceholders -Path $Path -Pattern $Pattern -Prepend $Prepend -Append $Append) + return (Convert-PodePlaceholder -Path $Path -Pattern $Pattern -Prepend $Prepend -Append $Append) } -function Convert-PodePlaceholders { +<# +.SYNOPSIS + Converts placeholders in a given path using a specified regex pattern. + +.DESCRIPTION + The `Convert-PodePlaceholder` function replaces placeholders in the provided path + with custom placeholders based on the specified regex pattern. You can customize + the prepend and append strings for the new placeholders. + +.PARAMETER Path + Specifies the path to convert. This parameter is mandatory. + +.PARAMETER Pattern + Specifies the regex pattern for identifying placeholders. If not provided, the default + placeholder regex pattern from `Get-PodePlaceholderRegex` is used. + +.PARAMETER Prepend + Specifies the string to prepend to the new placeholders. Default is '(?<'. + +.PARAMETER Append + Specifies the string to append to the new placeholders. Default is '>[^\/]+?)'. + +.OUTPUTS + Returns the path with replaced placeholders. + +.EXAMPLE + # Example usage: + $originalPath = '/api/users/{id}' + $convertedPath = Convert-PodePlaceholder -Path $originalPath + # Returns '/api/users/(?[^\/]+?)' with custom placeholders. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Convert-PodePlaceholder { + [CmdletBinding()] + [OutputType([string])] param( [Parameter(Mandatory = $true)] [string] @@ -2938,7 +3223,33 @@ function Convert-PodePlaceholders { return $Path } -function Test-PodePlaceholders { +<# +.SYNOPSIS + Tests whether a given path contains a placeholder based on a specified regex pattern. + +.DESCRIPTION + The `Test-PodePlaceholder` function checks if the provided path contains a placeholder + by matching it against a regex pattern. Placeholders are typically used for dynamic values. + +.PARAMETER Path + Specifies the path to test. This parameter is mandatory. + +.PARAMETER Placeholder + Specifies the regex pattern for identifying placeholders. If not provided, the default + placeholder regex pattern from `Get-PodePlaceholderRegex` is used. + +.OUTPUTS + Returns `$true` if the path contains a placeholder; otherwise, returns `$false`. + +.EXAMPLE + # Example usage: + $isPlaceholder = Test-PodePlaceholder -Path '/api/users/{id}' + # Returns $true because the path contains a placeholder. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Test-PodePlaceholder { param( [Parameter(Mandatory = $true)] [string] @@ -2981,68 +3292,33 @@ function Get-PodeModuleManifest { return $moduleManifest } - -<# -.SYNOPSIS -Tests if the Pode module is from the development branch. - -.DESCRIPTION -The Test-PodeVersionDev function checks if the Pode module's version matches the placeholder value ('$version$'), which is used to indicate the development branch of the module. It returns $true if the version matches, indicating the module is from the development branch, and $false otherwise. - -.PARAMETER None -This function does not accept any parameters. - -.OUTPUTS -System.Boolean -Returns $true if the Pode module version is '$version$', indicating the development branch. Returns $false for any other version. - -.EXAMPLE -PS> $moduleManifest = @{ ModuleVersion = '$version$' } -PS> Test-PodeVersionDev - -Returns $true, indicating the development branch. - -.EXAMPLE -PS> $moduleManifest = @{ ModuleVersion = '1.2.3' } -PS> Test-PodeVersionDev - -Returns $false, indicating a specific release version. - -.NOTES -This function assumes that $moduleManifest is a hashtable representing the loaded module manifest, with a key of ModuleVersion. - -#> -function Test-PodeVersionDev { - return (Get-PodeModuleManifest).ModuleVersion -eq '$version$' -} - <# .SYNOPSIS -Tests the running PowerShell version for compatibility with Pode, identifying end-of-life (EOL) and untested versions. + Tests the running PowerShell version for compatibility with Pode, identifying end-of-life (EOL) and untested versions. .DESCRIPTION -The `Test-PodeVersionPwshEOL` function checks the current PowerShell version against a list of versions that were either supported or EOL at the time of the Pode release. It uses the module manifest to determine which PowerShell versions are considered EOL and which are officially supported. If the current version is EOL or was not tested with the current release of Pode, the function generates a warning. This function aids in maintaining best practices for using supported PowerShell versions with Pode. + The `Test-PodeVersionPwshEOL` function checks the current PowerShell version against a list of versions that were either supported or EOL at the time of the Pode release. It uses the module manifest to determine which PowerShell versions are considered EOL and which are officially supported. If the current version is EOL or was not tested with the current release of Pode, the function generates a warning. This function aids in maintaining best practices for using supported PowerShell versions with Pode. .PARAMETER ReportUntested -If specified, the function will report if the current PowerShell version was not available and thus untested at the time of the Pode release. This is useful for identifying potential compatibility issues with newer versions of PowerShell. + If specified, the function will report if the current PowerShell version was not available and thus untested at the time of the Pode release. This is useful for identifying potential compatibility issues with newer versions of PowerShell. .OUTPUTS -A hashtable containing two keys: -- `eol`: A boolean indicating if the current PowerShell version was EOL at the time of the Pode release. -- `supported`: A boolean indicating if the current PowerShell version was officially supported by Pode at the time of the release. + A hashtable containing two keys: + - `eol`: A boolean indicating if the current PowerShell version was EOL at the time of the Pode release. + - `supported`: A boolean indicating if the current PowerShell version was officially supported by Pode at the time of the release. .EXAMPLE -Test-PodeVersionPwshEOL + Test-PodeVersionPwshEOL -Checks the current PowerShell version against Pode's supported and EOL versions list. Outputs a warning if the version is EOL or untested, and returns a hashtable indicating the compatibility status. + Checks the current PowerShell version against Pode's supported and EOL versions list. Outputs a warning if the version is EOL or untested, and returns a hashtable indicating the compatibility status. .EXAMPLE -Test-PodeVersionPwshEOL -ReportUntested + Test-PodeVersionPwshEOL -ReportUntested -Similar to the basic usage, but also reports if the current PowerShell version was untested because it was not available at the time of the Pode release. + Similar to the basic usage, but also reports if the current PowerShell version was untested because it was not available at the time of the Pode release. .NOTES -This function is part of the Pode module's utilities to ensure compatibility and encourage the use of supported PowerShell versions. + This function is part of the Pode module's utilities to ensure compatibility and encourage the use of supported PowerShell versions. #> function Test-PodeVersionPwshEOL { @@ -3062,14 +3338,16 @@ function Test-PodeVersionPwshEOL { $isEol = "$($psVersion.Major).$($psVersion.Minor)" -in $eolVersions if ($isEol) { - Write-PodeHost "[WARNING] Pode $(Get-PodeVersion) has not been tested on PowerShell $($PSVersionTable.PSVersion), as it is EOL." -ForegroundColor Yellow + # [WARNING] Pode version has not been tested on PowerShell version, as it is EOL + Write-PodeHost ($PodeLocale.eolPowerShellWarningMessage -f $PodeVersion, $PSVersion) -ForegroundColor Yellow } $SupportedVersions = $moduleManifest.PrivateData.PwshVersions.Supported -split ',' $isSupported = "$($psVersion.Major).$($psVersion.Minor)" -in $SupportedVersions if ((! $isSupported) -and (! $isEol) -and $ReportUntested) { - Write-PodeHost "[WARNING] Pode $(Get-PodeVersion) has not been tested on PowerShell $($PSVersionTable.PSVersion), as it was not available when Pode was released." -ForegroundColor Yellow + # [WARNING] Pode version has not been tested on PowerShell version, as it was not available when Pode was released + Write-PodeHost ($PodeLocale.untestedPowerShellVersionWarningMessage -f $PodeVersion, $PSVersion) -ForegroundColor Yellow } return @{ @@ -3081,15 +3359,19 @@ function Test-PodeVersionPwshEOL { <# .SYNOPSIS -creates a YAML description of the data in the object - based on https://github.com/Phil-Factor/PSYaml + creates a YAML description of the data in the object - based on https://github.com/Phil-Factor/PSYaml + .DESCRIPTION -This produces YAML from any object you pass to it. It isn't suitable for the huge objects produced by some of the cmdlets such as Get-Process, but fine for simple objects + This produces YAML from any object you pass to it. + .PARAMETER Object -the object that you want scripted out + The object that you want scripted out. This parameter accepts input via the pipeline. + .PARAMETER Depth -The depth that you want your object scripted to + The depth that you want your object scripted to + .EXAMPLE -Get-PodeOpenApiDefinition|ConvertTo-PodeYaml + Get-PodeOpenApiDefinition|ConvertTo-PodeYaml #> function ConvertTo-PodeYaml { [CmdletBinding()] @@ -3104,58 +3386,75 @@ function ConvertTo-PodeYaml { $Depth = 16 ) - if ($null -eq $PodeContext.Server.InternalCache.YamlModuleImported) { - $PodeContext.Server.InternalCache.YamlModuleImported = ((Test-PodeModuleInstalled -Name 'PSYaml') -or (Test-PodeModuleInstalled -Name 'powershell-yaml')) + begin { + $pipelineObject = @() } - if ($PodeContext.Server.InternalCache.YamlModuleImported) { - return ($InputObject | ConvertTo-Yaml) + process { + $pipelineObject += $_ } - else { - return ConvertTo-PodeYamlInternal -InputObject $InputObject -Depth $Depth -NoNewLine + + end { + if ($pipelineObject.Count -gt 1) { + $InputObject = $pipelineObject + } + + if ($PodeContext.Server.Web.OpenApi.UsePodeYamlInternal) { + return ConvertTo-PodeYamlInternal -InputObject $InputObject -Depth $Depth -NoNewLine + } + + if ($null -eq $PodeContext.Server.InternalCache.YamlModuleImported) { + $PodeContext.Server.InternalCache.YamlModuleImported = ((Test-PodeModuleInstalled -Name 'PSYaml') -or (Test-PodeModuleInstalled -Name 'powershell-yaml')) + } + + if ($PodeContext.Server.InternalCache.YamlModuleImported) { + return ($InputObject | ConvertTo-Yaml) + } + else { + return ConvertTo-PodeYamlInternal -InputObject $InputObject -Depth $Depth -NoNewLine + } } } <# .SYNOPSIS - Converts PowerShell objects into a YAML-formatted string. + Converts PowerShell objects into a YAML-formatted string. .DESCRIPTION - This function takes PowerShell objects and converts them to a YAML string representation. - It supports various data types including arrays, hashtables, strings, and more. - The depth of conversion can be controlled, allowing for nested objects to be accurately represented. + This function takes PowerShell objects and converts them to a YAML string representation. + It supports various data types including arrays, hashtables, strings, and more. + The depth of conversion can be controlled, allowing for nested objects to be accurately represented. .PARAMETER InputObject - The PowerShell object to convert to YAML. This parameter accepts input via the pipeline. + The PowerShell object to convert to YAML. .PARAMETER Depth - Specifies the maximum depth of object nesting to convert. Default is 10 levels deep. + Specifies the maximum depth of object nesting to convert. Default is 10 levels deep. .PARAMETER NestingLevel - Used internally to track the current depth of recursion. Generally not specified by the user. + Used internally to track the current depth of recursion. Generally not specified by the user. .PARAMETER NoNewLine - If specified, suppresses the newline characters in the output to create a single-line string. + If specified, suppresses the newline characters in the output to create a single-line string. .OUTPUTS - System.String. Returns a string in YAML format. + System.String. Returns a string in YAML format. .EXAMPLE - $object | ConvertTo-PodeYamlInternal + ConvertTo-PodeYamlInternal -InputObject $object - Converts the object piped to it into a YAML string. + Converts the object into a YAML string. .NOTES - This is an internal function and may change in future releases of Pode. - It converts only basic PowerShell types, such as strings, integers, booleans, arrays, hashtables, and ordered dictionaries into a YAML format. + This is an internal function and may change in future releases of Pode. + It converts only basic PowerShell types, such as strings, integers, booleans, arrays, hashtables, and ordered dictionaries into a YAML format. #> - function ConvertTo-PodeYamlInternal { [CmdletBinding()] [OutputType([string])] param ( - [parameter(Mandatory = $true, ValueFromPipeline = $true)] + [parameter(Mandatory = $true)] [AllowNull()] $InputObject, @@ -3172,148 +3471,308 @@ function ConvertTo-PodeYamlInternal { $NoNewLine ) - process { - # if it is null return null - If ( !($InputObject) ) { - if ($InputObject -is [Object[]]) { - return '[]' - } - else { - return '' - } + #report the leaves in terms of object type + if ($Depth -ilt $NestingLevel) { + return '' + } + # if it is null return null + If ( !($InputObject) ) { + if ($InputObject -is [Object[]]) { + return '[]' } + else { + return '' + } + } - $padding = [string]::new(' ', $NestingLevel * 2) # lets just create our left-padding for the block - try { - $Type = $InputObject.GetType().Name # we start by getting the object's type - if ($InputObject -is [object[]]) { - #what it really is - $Type = "$($InputObject.GetType().BaseType.Name)" - } + $padding = [string]::new(' ', $NestingLevel * 2) # lets just create our left-padding for the block + try { + $Type = $InputObject.GetType().Name # we start by getting the object's type + if ($InputObject -is [object[]]) { + #what it really is + $Type = "$($InputObject.GetType().BaseType.Name)" + } - #report the leaves in terms of object type - if ($Depth -ilt $NestingLevel) { - $Type = 'OutOfDepth' - } + # Check for specific value types string + if ($Type -ne 'String') { # prevent these values being identified as an object if ($InputObject -is [System.Collections.Specialized.OrderedDictionary]) { - $Type = 'HashTable' + $Type = 'hashTable' } elseif ($Type -ieq 'List`1') { - $Type = 'Array' + $Type = 'array' } elseif ($InputObject -is [array]) { - $Type = 'Array' + $Type = 'array' } # whatever it thinks it is called - elseif ($InputObject -is [hashtable]) { - $Type = 'HashTable' + elseif ($InputObject -is [hashtable] ) { + $Type = 'hashTable' } # for our purposes it is a hashtable + } + + $output += switch ($Type.ToLower()) { + 'string' { + $String = "$InputObject" + if (($string -match '[\r\n]' -or $string.Length -gt 80) -and ($string -notlike 'http*')) { + $multiline = [System.Text.StringBuilder]::new("|`n") - $output += switch ($Type.ToLower()) { - 'string' { - $String = "$InputObject" - if (($string -match '[\r\n]' -or $string.Length -gt 80) -and ($string -notlike 'http*')) { - $multiline = [System.Text.StringBuilder]::new("|`n") - - $items = $string.Split("`n") - for ($i = 0; $i -lt $items.Length; $i++) { - $workingString = $items[$i] -replace '\r$' - $length = $workingString.Length - $index = 0 - $wrap = 80 - - while ($index -lt $length) { - $breakpoint = $wrap - $linebreak = $false - - if (($length - $index) -gt $wrap) { - $lastSpaceIndex = $workingString.LastIndexOf(' ', $index + $wrap, $wrap) - if ($lastSpaceIndex -ne -1) { - $breakpoint = $lastSpaceIndex - $index - } - else { - $linebreak = $true - $breakpoint-- - } + $items = $string.Split("`n") + for ($i = 0; $i -lt $items.Length; $i++) { + $workingString = $items[$i] -replace '\r$' + $length = $workingString.Length + $index = 0 + $wrap = 80 + + while ($index -lt $length) { + $breakpoint = $wrap + $linebreak = $false + + if (($length - $index) -gt $wrap) { + $lastSpaceIndex = $workingString.LastIndexOf(' ', $index + $wrap, $wrap) + if ($lastSpaceIndex -ne -1) { + $breakpoint = $lastSpaceIndex - $index } else { - $breakpoint = $length - $index - } - - $null = $multiline.Append($padding).Append($workingString.Substring($index, $breakpoint).Trim()) - if ($linebreak) { - $null = $multiline.Append('\') + $linebreak = $true + $breakpoint-- } + } + else { + $breakpoint = $length - $index + } - $index += $breakpoint - if ($index -lt $length) { - $null = $multiline.Append([System.Environment]::NewLine) - } + $null = $multiline.Append($padding).Append($workingString.Substring($index, $breakpoint).Trim()) + if ($linebreak) { + $null = $multiline.Append('\') } - if ($i -lt ($items.Length - 1)) { + $index += $breakpoint + if ($index -lt $length) { $null = $multiline.Append([System.Environment]::NewLine) } } - $multiline.ToString().TrimEnd() - break + if ($i -lt ($items.Length - 1)) { + $null = $multiline.Append([System.Environment]::NewLine) + } + } + + $multiline.ToString().TrimEnd() + break + } + else { + if ($string -match '^[#\[\]@\{\}\!\*]') { + "'$($string -replace '''', '''''')'" } else { - if ($string -match '^[#\[\]@\{\}\!\*]') { - "'$($string -replace '''', '''''')'" - } - else { - $string - } - break + $string } break } - 'hashtable' { - if ($InputObject.Count -gt 0 ) { - $index = 0 - $string = [System.Text.StringBuilder]::new() - foreach ($item in $InputObject.Keys) { + break + } + + 'hashtable' { + if ($InputObject.Count -gt 0 ) { + $index = 0 + $string = [System.Text.StringBuilder]::new() + foreach ($item in $InputObject.Keys) { + if ($NoNewLine -and $index++ -eq 0) { $NewPadding = '' } else { $NewPadding = "`n$padding" } + $null = $string.Append( $NewPadding).Append( $item).Append(': ') + if ($InputObject[$item] -is [System.ValueType]) { + if ($InputObject[$item] -is [bool]) { + $null = $string.Append($InputObject[$item].ToString().ToLower()) + } + else { + $null = $string.Append($InputObject[$item]) + } + } + else { if ($InputObject[$item] -is [string]) { $increment = 2 } else { $increment = 1 } - if ($NoNewLine -and $index++ -eq 0) { $NewPadding = '' } else { $NewPadding = "`n$padding" } - $null = $string.Append( $NewPadding).Append( $item).Append(': ').Append((ConvertTo-PodeYamlInternal -InputObject $InputObject[$item] -Depth $Depth -NestingLevel ($NestingLevel + $increment))) + $null = $string.Append((ConvertTo-PodeYamlInternal -InputObject $InputObject[$item] -Depth $Depth -NestingLevel ($NestingLevel + $increment))) } - $string.ToString() } - else { '{}' } - break - } - 'boolean' { - if ($InputObject -eq $true) { 'true' } else { 'false' } - break + $string.ToString() } - 'array' { - $string = [System.Text.StringBuilder]::new() + else { '{}' } + break + } + + 'pscustomobject' { + if ($InputObject.Count -gt 0 ) { $index = 0 - foreach ($item in $InputObject ) { + $string = [System.Text.StringBuilder]::new() + foreach ($item in ($InputObject | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name)) { if ($NoNewLine -and $index++ -eq 0) { $NewPadding = '' } else { $NewPadding = "`n$padding" } - $null = $string.Append($NewPadding).Append('- ').Append((ConvertTo-PodeYamlInternal -InputObject $item -depth $Depth -NestingLevel ($NestingLevel + 1) -NoNewLine)) + $null = $string.Append( $NewPadding).Append( $item).Append(': ') + if ($InputObject.$item -is [System.ValueType]) { + if ($InputObject.$item -is [bool]) { + $null = $string.Append($InputObject.$item.ToString().ToLower()) + } + else { + $null = $string.Append($InputObject.$item) + } + } + else { + if ($InputObject.$item -is [string]) { $increment = 2 } else { $increment = 1 } + $null = $string.Append((ConvertTo-PodeYamlInternal -InputObject $InputObject.$item -Depth $Depth -NestingLevel ($NestingLevel + $increment))) + } } $string.ToString() - break } - 'int32' { - $InputObject - } - 'double' { - $InputObject - } - default { - "'$InputObject'" + else { '{}' } + break + } + + 'array' { + $string = [System.Text.StringBuilder]::new() + $index = 0 + foreach ($item in $InputObject ) { + if ($NoNewLine -and $index++ -eq 0) { $NewPadding = '' } else { $NewPadding = "`n$padding" } + $null = $string.Append($NewPadding).Append('- ').Append((ConvertTo-PodeYamlInternal -InputObject $item -depth $Depth -NestingLevel ($NestingLevel + 1) -NoNewLine)) } + $string.ToString() + break + } + + default { + "'$InputObject'" } - return $Output } - catch { - $_ | Write-PodeErrorLog - $_.Exception | Write-PodeErrorLog -CheckInnerException - throw "Error'$($_)' in script $($_.InvocationInfo.ScriptName) $($_.InvocationInfo.Line.Trim()) (line $($_.InvocationInfo.ScriptLineNumber)) char $($_.InvocationInfo.OffsetInLine) executing $($_.InvocationInfo.MyCommand) on $type object '$($InputObject)' Class: $($InputObject.GetType().Name) BaseClass: $($InputObject.GetType().BaseType.Name) " + return $Output + } + catch { + $_ | Write-PodeErrorLog + $_.Exception | Write-PodeErrorLog -CheckInnerException + throw ($PodeLocale.scriptErrorExceptionMessage -f $_, $_.InvocationInfo.ScriptName, $_.InvocationInfo.Line.Trim(), $_.InvocationInfo.ScriptLineNumber, $_.InvocationInfo.OffsetInLine, $_.InvocationInfo.MyCommand, $type, $InputObject, $InputObject.GetType().Name, $InputObject.GetType().BaseType.Name) + } +} + + +<# +.SYNOPSIS + Resolves various types of object arrays into PowerShell objects. + +.DESCRIPTION + This function takes an input property and determines its type. + It then resolves the property into a PowerShell object or an array of objects, + depending on whether the property is a hashtable, array, or single object. + +.PARAMETER Property + The property to be resolved. It can be a hashtable, an object array, or a single object. + +.RETURNS + Returns a PowerShell object or an array of PowerShell objects, depending on the input property type. + +.EXAMPLE + $result = Resolve-PodeObjectArray -Property $myProperty + This example resolves the $myProperty into a PowerShell object or an array of objects. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Resolve-PodeObjectArray { + [CmdletBinding()] + [OutputType([object[]])] + [OutputType([psobject])] + param ( + [AllowNull()] + [object] + $Property + ) + + # Check if the property is a hashtable + if ($Property -is [hashtable]) { + # If the hashtable has only one item, convert it to a PowerShell object + if ($Property.Count -eq 1) { + return [pscustomobject]$Property } + else { + # If the hashtable has more than one item, recursively resolve each item + return @(foreach ($p in $Property) { + Resolve-PodeObjectArray -Property $p + }) + } + } + # Check if the property is an array of objects + elseif ($Property -is [object[]]) { + # Recursively resolve each item in the array + return @(foreach ($p in $Property) { + Resolve-PodeObjectArray -Property $p + }) + } + # Check if the property is already a PowerShell object + elseif ($Property -is [psobject]) { + return $Property + } + else { + # For any other type, convert it to a PowerShell object + return [pscustomobject]$Property + } +} + +<# +.SYNOPSIS + Creates a deep clone of a PSObject by serializing and deserializing the object. + +.DESCRIPTION + The Copy-PodeObjectDeepClone function takes a PSObject as input and creates a deep clone of it. + This is achieved by serializing the object using the PSSerializer class, and then + deserializing it back into a new instance. This method ensures that nested objects, arrays, + and other complex structures are copied fully, without sharing references between the original + and the cloned object. + +.PARAMETER InputObject + The PSObject that you want to deep clone. This object will be serialized and then deserialized + to create a deep copy. + +.PARAMETER Depth + Specifies the depth for the serialization. The depth controls how deeply nested objects + and properties are serialized. The default value is 10. + +.INPUTS + [PSObject] - The function accepts a PSObject to deep clone. + +.OUTPUTS + [PSObject] - The function returns a new PSObject that is a deep clone of the original. + +.EXAMPLE + $originalObject = [PSCustomObject]@{ + Name = 'John Doe' + Age = 30 + Address = [PSCustomObject]@{ + Street = '123 Main St' + City = 'Anytown' + Zip = '12345' + } + } + + $clonedObject = $originalObject | Copy-PodeObjectDeepClone -Deep 15 + + # The $clonedObject is now a deep clone of $originalObject. + # Changes to $clonedObject will not affect $originalObject and vice versa. + +.NOTES + This function uses the System.Management.Automation.PSSerializer class, which is available in + PowerShell 5.1 and later versions. The default depth parameter is set to 10 to handle nested + objects appropriately, but it can be customized via the -Deep parameter. + This is an internal function and may change in future releases of Pode. +#> +function Copy-PodeObjectDeepClone { + param ( + [Parameter(Mandatory, ValueFromPipeline)] + [PSObject]$InputObject, + + [Parameter()] + [int]$Depth = 10 + ) + + process { + # Serialize the object to XML format using PSSerializer + # The depth parameter controls how deeply nested objects are serialized + $xmlSerializer = [System.Management.Automation.PSSerializer]::Serialize($InputObject, $Depth) + + # Deserialize the XML back into a new PSObject, creating a deep clone of the original + return [System.Management.Automation.PSSerializer]::Deserialize($xmlSerializer) } } \ No newline at end of file diff --git a/src/Private/Logging.ps1 b/src/Private/Logging.ps1 index ad67d42ad..27db3e4fa 100644 --- a/src/Private/Logging.ps1 +++ b/src/Private/Logging.ps1 @@ -104,6 +104,7 @@ function Get-PodeLoggingEventViewerMethod { $entryLog.WriteEvent($entryInstance, $message) } catch { + $_ | Write-PodeErrorLog -Level Debug } } } @@ -207,7 +208,25 @@ function Get-PodeErrorLoggingName { return '__pode_log_errors__' } +<# +.SYNOPSIS + Retrieves a Pode logger by name. + +.DESCRIPTION + This function allows you to retrieve a Pode logger by specifying its name. It returns the logger object associated with the given name. + +.PARAMETER Name + The name of the Pode logger to retrieve. + +.OUTPUTS + A Pode logger object. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> function Get-PodeLogger { + [CmdletBinding()] + [OutputType([hashtable])] param( [Parameter(Mandatory = $true)] [string] @@ -227,7 +246,23 @@ function Test-PodeLoggerEnabled { return ($PodeContext.Server.Logging.Enabled -and $PodeContext.Server.Logging.Types.ContainsKey($Name)) } -function Get-PodeErrorLoggingLevels { +<# +.SYNOPSIS + Gets the error logging levels for Pode. + +.DESCRIPTION + This function retrieves the error logging levels configured for Pode. It returns an array of available error levels. + +.PARAMETER Name + The name of the Pode logger to retrieve. + +.OUTPUTS + An array of error logging levels. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Get-PodeErrorLoggingLevel { return (Get-PodeLogger -Name (Get-PodeErrorLoggingName)).Arguments.Levels } @@ -340,79 +375,105 @@ function Start-PodeLoggingRunspace { } $script = { - while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { - # if there are no logs to process, just sleep for a few seconds - but after checking the batch - if ($PodeContext.LogsToProcess.Count -eq 0) { - Test-PodeLoggerBatches - Start-Sleep -Seconds 5 - continue - } + try { + while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + try { + # if there are no logs to process, just sleep for a few seconds - but after checking the batch + if ($PodeContext.LogsToProcess.Count -eq 0) { + Test-PodeLoggerBatch + Start-Sleep -Seconds 5 + continue + } - # safely pop off the first log from the array - $log = (Lock-PodeObject -Return -Object $PodeContext.LogsToProcess -ScriptBlock { - $log = $PodeContext.LogsToProcess[0] - $null = $PodeContext.LogsToProcess.RemoveAt(0) - return $log - }) - - # run the log item through the appropriate method - $logger = Get-PodeLogger -Name $log.Name - $now = [datetime]::Now - - # if the log is null, check batch then sleep and skip - if ($null -eq $log) { - Start-Sleep -Milliseconds 100 - continue - } + # safely pop off the first log from the array + $log = (Lock-PodeObject -Return -Object $PodeContext.LogsToProcess -ScriptBlock { + $log = $PodeContext.LogsToProcess[0] + $null = $PodeContext.LogsToProcess.RemoveAt(0) + return $log + }) + + # run the log item through the appropriate method + $logger = Get-PodeLogger -Name $log.Name + $now = [datetime]::Now + + # if the log is null, check batch then sleep and skip + if ($null -eq $log) { + Start-Sleep -Milliseconds 100 + continue + } - # convert to log item into a writable format - $rawItems = $log.Item - $_args = @($log.Item) + @($logger.Arguments) - $result = @(Invoke-PodeScriptBlock -ScriptBlock $logger.ScriptBlock -Arguments $_args -UsingVariables $logger.UsingVariables -Return -Splat) - - # check batching - $batch = $logger.Method.Batch - if ($batch.Size -gt 1) { - # add current item to batch - $batch.Items += $result - $batch.RawItems += $log.Item - $batch.LastUpdate = $now - - # if the current amount of items matches the batch, write - $result = $null - if ($batch.Items.Length -ge $batch.Size) { - $result = $batch.Items - $rawItems = $batch.RawItems - } + # convert to log item into a writable format + $rawItems = $log.Item + $_args = @($log.Item) + @($logger.Arguments) + $result = @(Invoke-PodeScriptBlock -ScriptBlock $logger.ScriptBlock -Arguments $_args -UsingVariables $logger.UsingVariables -Return -Splat) + + # check batching + $batch = $logger.Method.Batch + if ($batch.Size -gt 1) { + # add current item to batch + $batch.Items += $result + $batch.RawItems += $log.Item + $batch.LastUpdate = $now + + # if the current amount of items matches the batch, write + $result = $null + if ($batch.Items.Length -ge $batch.Size) { + $result = $batch.Items + $rawItems = $batch.RawItems + } + + # if we're writing, reset the items + if ($null -ne $result) { + $batch.Items = @() + $batch.RawItems = @() + } + } - # if we're writing, reset the items - if ($null -ne $result) { - $batch.Items = @() - $batch.RawItems = @() - } - } + # send the writable log item off to the log writer + if ($null -ne $result) { + $_args = @(, $result) + @($logger.Method.Arguments) + @(, $rawItems) + $null = Invoke-PodeScriptBlock -ScriptBlock $logger.Method.ScriptBlock -Arguments $_args -UsingVariables $logger.Method.UsingVariables -Splat + } - # send the writable log item off to the log writer - if ($null -ne $result) { - $_args = @(, $result) + @($logger.Method.Arguments) + @(, $rawItems) - $null = Invoke-PodeScriptBlock -ScriptBlock $logger.Method.ScriptBlock -Arguments $_args -UsingVariables $logger.Method.UsingVariables -Splat + # small sleep to lower cpu usage + Start-Sleep -Milliseconds 100 + } + catch { + $_ | Write-PodeErrorLog + } } - - # small sleep to lower cpu usage - Start-Sleep -Milliseconds 100 + } + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } + catch { + $_ | Write-PodeErrorLog + throw $_.Exception } } - Add-PodeRunspace -Type Main -ScriptBlock $script + Add-PodeRunspace -Type Main -Name 'Logging' -ScriptBlock $script } -function Test-PodeLoggerBatches { +<# +.SYNOPSIS + Tests whether Pode logger batches need to be written. + +.DESCRIPTION + This function checks each Pode logger and determines if its batch needs to be written. It evaluates the batch size, timeout, and last update timestamp to decide whether to process the batch and write the log entries. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Test-PodeLoggerBatch { $now = [datetime]::Now # check each logger, and see if its batch needs to be written foreach ($logger in $PodeContext.Server.Logging.Types.Values) { $batch = $logger.Method.Batch - if (($batch.Size -gt 1) -and ($batch.Items.Length -gt 0) -and ($batch.Timeout -gt 0) -and ($null -ne $batch.LastUpdate) -and ($batch.LastUpdate.AddSeconds($batch.Timeout) -le $now)) { + if (($batch.Size -gt 1) -and ($batch.Items.Length -gt 0) -and ($batch.Timeout -gt 0) ` + -and ($null -ne $batch.LastUpdate) -and ($batch.LastUpdate.AddSeconds($batch.Timeout) -le $now) + ) { $result = $batch.Items $rawItems = $batch.RawItems diff --git a/src/Private/Mappers.ps1 b/src/Private/Mappers.ps1 index 2590a2b5a..810146ae5 100644 --- a/src/Private/Mappers.ps1 +++ b/src/Private/Mappers.ps1 @@ -39,7 +39,7 @@ function Get-PodeContentType { '.accdw' { return 'application/msaccess.webapplication' } '.accft' { return 'application/msaccess.ftemplate' } '.acx' { return 'application/internet-property-stream' } - '.addin' { return 'text/xml' } + '.addin' { return 'application/xml' } '.ade' { return 'application/msaccess' } '.adobebridge' { return 'application/x-bridge-url' } '.adp' { return 'application/msaccess' } @@ -123,10 +123,10 @@ function Get-PodeContentType { '.dib' { return 'image/bmp' } '.dif' { return 'video/x-dv' } '.dir' { return 'application/x-director' } - '.disco' { return 'text/xml' } + '.disco' { return 'application/xml' } '.divx' { return 'video/divx' } '.dll' { return 'application/x-msdownload' } - '.dll.config' { return 'text/xml' } + '.dll.config' { return 'application/xml' } '.dlm' { return 'text/dlm' } '.doc' { return 'application/msword' } '.docm' { return 'application/vnd.ms-word.document.macroEnabled.12' } @@ -136,8 +136,8 @@ function Get-PodeContentType { '.dotx' { return 'application/vnd.openxmlformats-officedocument.wordprocessingml.template' } '.dsp' { return 'application/octet-stream' } '.dsw' { return 'text/plain' } - '.dtd' { return 'text/xml' } - '.dtsconfig' { return 'text/xml' } + '.dtd' { return 'application/xml' } + '.dtsconfig' { return 'application/xml' } '.dv' { return 'video/x-dv' } '.dvi' { return 'application/x-dvi' } '.dwf' { return 'drawing/x-dwf' } @@ -153,7 +153,7 @@ function Get-PodeContentType { '.etx' { return 'text/x-setext' } '.evy' { return 'application/envoy' } '.exe' { return 'application/octet-stream' } - '.exe.config' { return 'text/xml' } + '.exe.config' { return 'application/xml' } '.fdf' { return 'application/vnd.fdf' } '.fif' { return 'application/fractals' } '.filters' { return 'application/xml' } @@ -284,7 +284,7 @@ function Get-PodeContentType { '.mka' { return 'audio/x-matroska' } '.mkv' { return 'video/x-matroska' } '.mmf' { return 'application/x-smaf' } - '.mno' { return 'text/xml' } + '.mno' { return 'application/xml' } '.mny' { return 'application/x-msmoney' } '.mod' { return 'video/mpeg' } '.mov' { return 'video/quicktime' } @@ -479,7 +479,7 @@ function Get-PodeContentType { '.spx' { return 'audio/ogg' } '.src' { return 'application/x-wais-source' } '.srf' { return 'text/plain' } - '.ssisdeploymentmanifest' { return 'text/xml' } + '.ssisdeploymentmanifest' { return 'application/xml' } '.ssm' { return 'application/streamingmedia' } '.sst' { return 'application/vnd.ms-pki.certstore' } '.stl' { return 'application/vnd.ms-pki.stl' } @@ -531,22 +531,22 @@ function Get-PodeContentType { '.vdp' { return 'text/plain' } '.vdproj' { return 'text/plain' } '.vdx' { return 'application/vnd.ms-visio.viewer' } - '.vml' { return 'text/xml' } + '.vml' { return 'application/xml' } '.vscontent' { return 'application/xml' } - '.vsct' { return 'text/xml' } + '.vsct' { return 'application/xml' } '.vsd' { return 'application/vnd.visio' } '.vsi' { return 'application/ms-vsi' } '.vsix' { return 'application/vsix' } - '.vsixlangpack' { return 'text/xml' } - '.vsixmanifest' { return 'text/xml' } + '.vsixlangpack' { return 'application/xml' } + '.vsixmanifest' { return 'application/xml' } '.vsmdi' { return 'application/xml' } '.vspscc' { return 'text/plain' } '.vss' { return 'application/vnd.visio' } '.vsscc' { return 'text/plain' } - '.vssettings' { return 'text/xml' } + '.vssettings' { return 'application/xml' } '.vssscc' { return 'text/plain' } '.vst' { return 'application/vnd.visio' } - '.vstemplate' { return 'text/xml' } + '.vstemplate' { return 'application/xml' } '.vsto' { return 'application/x-ms-vsto' } '.vsw' { return 'application/vnd.visio' } '.vsx' { return 'application/vnd.visio' } @@ -590,7 +590,7 @@ function Get-PodeContentType { '.wrl' { return 'x-world/x-vrml' } '.wrz' { return 'x-world/x-vrml' } '.wsc' { return 'text/scriptlet' } - '.wsdl' { return 'text/xml' } + '.wsdl' { return 'application/xml' } '.wvx' { return 'video/x-ms-wvx' } '.x' { return 'application/directx' } '.xaf' { return 'x-world/x-vrml' } @@ -616,26 +616,26 @@ function Get-PodeContentType { '.xltm' { return 'application/vnd.ms-excel.template.macroEnabled.12' } '.xltx' { return 'application/vnd.openxmlformats-officedocument.spreadsheetml.template' } '.xlw' { return 'application/vnd.ms-excel' } - '.xml' { return 'text/xml' } + '.xml' { return 'application/xml' } '.xmp' { return 'application/octet-stream' } '.xmta' { return 'application/xml' } '.xof' { return 'x-world/x-vrml' } '.xoml' { return 'text/plain' } '.xpm' { return 'image/x-xpixmap' } '.xps' { return 'application/vnd.ms-xpsdocument' } - '.xrm-ms' { return 'text/xml' } + '.xrm-ms' { return 'application/xml' } '.xsc' { return 'application/xml' } - '.xsd' { return 'text/xml' } - '.xsf' { return 'text/xml' } - '.xsl' { return 'text/xml' } - '.xslt' { return 'text/xml' } + '.xsd' { return 'application/xml' } + '.xsf' { return 'application/xml' } + '.xsl' { return 'application/xml' } + '.xslt' { return 'application/xml' } '.xsn' { return 'application/octet-stream' } '.xss' { return 'application/xml' } '.xspf' { return 'application/xspf+xml' } '.xtp' { return 'application/octet-stream' } '.xwd' { return 'image/x-xwindowdump' } - '.yaml' { return 'application/x-yaml' } - '.yml' { return 'application/x-yaml' } + '.yaml' { return 'application/yaml' } #RFC 9512 + '.yml' { return 'application/yaml' } '.z' { return 'application/x-compress' } '.zip' { return 'application/zip' } default { return (Resolve-PodeValue -Check $DefaultIsNull -TrueValue $null -FalseValue 'text/plain') } diff --git a/src/Private/Metrics.ps1 b/src/Private/Metrics.ps1 index d510ddd29..3b004d3b5 100644 --- a/src/Private/Metrics.ps1 +++ b/src/Private/Metrics.ps1 @@ -1,4 +1,40 @@ -function Update-PodeServerRequestMetrics { +<# +.SYNOPSIS + Updates server request metrics based on the provided web event. + +.DESCRIPTION + The `Update-PodeServerRequestMetric` function increments relevant metrics associated with server requests. + It takes a web event (represented as a hashtable) and updates the appropriate metrics. + +.PARAMETER WebEvent + Specifies the web event to process. This parameter is optional. + +.INPUTS + None. You cannot pipe objects to Update-PodeServerRequestMetric. + +.OUTPUTS + None. The function modifies the state of metrics in the PodeContext. + +.EXAMPLE + # Example usage: + $webEvent = @{ + Response = @{ + StatusCode = 200 + } + Route = @{ + Metrics = @{ + Requests = $routeMetrics + } + } + } + + Update-PodeServerRequestMetric -WebEvent $webEvent + # Metrics associated with the web event are updated. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Update-PodeServerRequestMetric { param( [Parameter()] [hashtable] @@ -9,16 +45,16 @@ function Update-PodeServerRequestMetrics { return } - # status code + # Extract the status code from the web event $status = "$($WebEvent.Response.StatusCode)" - # metrics to update + # Determine which metrics to update $metrics = @($PodeContext.Metrics.Requests) if ($null -ne $WebEvent.Route) { $metrics += $WebEvent.Route.Metrics.Requests } - # increment the request metrics + # Increment the request metrics and status code counts foreach ($metric in $metrics) { Lock-PodeObject -Object $metric -ScriptBlock { $metric.Total++ @@ -32,7 +68,40 @@ function Update-PodeServerRequestMetrics { } } -function Update-PodeServerSignalMetrics { +<# +.SYNOPSIS + Updates server signal metrics based on the provided signal event. + +.DESCRIPTION + The `Update-PodeServerSignalMetric` function increments relevant metrics associated with server signals. + It takes a signal event (represented as a hashtable) and updates the appropriate metrics. + +.PARAMETER SignalEvent + Specifies the signal event to process. This parameter is optional. + +.INPUTS + None. You cannot pipe objects to Update-PodeServerSignalMetric. + +.OUTPUTS + None. The function modifies the state of metrics in the PodeContext. + +.EXAMPLE + # Example usage: + $signalEvent = @{ + Route = @{ + Metrics = @{ + Requests = $routeMetrics + } + } + } + + Update-PodeServerSignalMetric -SignalEvent $signalEvent + # Metrics associated with the signal event are updated. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Update-PodeServerSignalMetric { param( [Parameter()] [hashtable] @@ -43,7 +112,7 @@ function Update-PodeServerSignalMetrics { return } - # metrics to update + # Determine which metrics to update $metrics = @($PodeContext.Metrics.Signals) if ($null -ne $SignalEvent.Route) { $metrics += $SignalEvent.Route.Metrics.Requests diff --git a/src/Private/Middleware.ps1 b/src/Private/Middleware.ps1 index 4f5d74599..db1066fb2 100644 --- a/src/Private/Middleware.ps1 +++ b/src/Private/Middleware.ps1 @@ -58,6 +58,7 @@ function Invoke-PodeMiddleware { } function New-PodeMiddlewareInternal { + [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory = $true)] @@ -78,7 +79,8 @@ function New-PodeMiddlewareInternal { ) if (Test-PodeIsEmpty $ScriptBlock) { - throw '[Middleware]: No ScriptBlock supplied' + # No ScriptBlock supplied + throw ($PodeLocale.noScriptBlockSuppliedExceptionMessage) } # if route is empty, set it to root @@ -146,7 +148,27 @@ function Get-PodeAccessMiddleware { }) } +<# +.SYNOPSIS +Retrieves the rate limit middleware for Pode. + +.DESCRIPTION +This function returns the inbuilt rate limit middleware for Pode. It checks if the request IP address, route, and endpoint have hit their respective rate limits. If any of these checks fail, a 429 status code is set, and the request is denied. + +.EXAMPLE +Get-PodeLimitMiddleware +Retrieves the rate limit middleware and adds it to the middleware pipeline. + +.RETURNS +[ScriptBlock] - Returns a script block that represents the rate limit middleware. + +.NOTES +This is an internal function and may change in future releases of Pode. +#> function Get-PodeLimitMiddleware { + [CmdletBinding()] + [OutputType([hashtable])] + param() return (Get-PodeInbuiltMiddleware -Name '__pode_mw_rate_limit__' -ScriptBlock { # are there any rules? if ($PodeContext.Server.Limits.Rules.Count -eq 0) { @@ -191,6 +213,9 @@ function Get-PodeLimitMiddleware { Retrieves middleware for serving public static content. #> function Get-PodePublicMiddleware { + [CmdletBinding()] + [OutputType([hashtable])] + param() return (Get-PodeInbuiltMiddleware -Name '__pode_mw_static_content__' -ScriptBlock { # only find public static content here $path = Find-PodePublicRoute -Path $WebEvent.Path @@ -392,7 +417,8 @@ function Initialize-PodeIISMiddleware { # fail if no iis token - because there should be! if ([string]::IsNullOrWhiteSpace($PodeContext.Server.IIS.Token)) { - throw 'IIS ASPNETCORE_TOKEN is missing' + # IIS ASPNETCORE_TOKEN is missing + throw ($PodeLocale.iisAspnetcoreTokenMissingExceptionMessage) } # add middleware to check every request has the token diff --git a/src/Private/OpenApi.ps1 b/src/Private/OpenApi.ps1 index c0c14be1b..2acb348d3 100644 --- a/src/Private/OpenApi.ps1 +++ b/src/Private/OpenApi.ps1 @@ -1,36 +1,36 @@ <# .SYNOPSIS -Converts content into an OpenAPI schema object format. + Converts content into an OpenAPI schema object format. .DESCRIPTION -The ConvertTo-PodeOAObjectSchema function takes a hashtable representing content and converts it into a format suitable for OpenAPI schema objects. -It validates the content types, processes array structures, and converts each property or reference into the appropriate OpenAPI schema format. -The function is designed to handle complex content structures for OpenAPI documentation within the Pode framework. + The ConvertTo-PodeOAObjectSchema function takes a hashtable representing content and converts it into a format suitable for OpenAPI schema objects. + It validates the content types, processes array structures, and converts each property or reference into the appropriate OpenAPI schema format. + The function is designed to handle complex content structures for OpenAPI documentation within the Pode framework. .PARAMETER Content -A hashtable representing the content to be converted into an OpenAPI schema object. The content can include various types and structures. + A hashtable representing the content to be converted into an OpenAPI schema object. The content can include various types and structures. .PARAMETER Properties -A switch to indicate if the content represents properties of an object schema. + A switch to indicate if the content represents properties of an object schema. .PARAMETER DefinitionTag -A string representing the definition tag to be used in the conversion process. This tag is essential for correctly formatting the content according to OpenAPI specifications. + A string representing the definition tag to be used in the conversion process. This tag is essential for correctly formatting the content according to OpenAPI specifications. .EXAMPLE -$schemaObject = ConvertTo-PodeOAObjectSchema -Content $myContent -DefinitionTag 'myTag' + $schemaObject = ConvertTo-PodeOAObjectSchema -Content $myContent -DefinitionTag 'myTag' -Converts a hashtable of content into an OpenAPI schema object using the definition tag 'myTag'. + Converts a hashtable of content into an OpenAPI schema object using the definition tag 'myTag'. .NOTES -This is an internal function and may change in future releases of Pode. + This is an internal function and may change in future releases of Pode. #> function ConvertTo-PodeOAObjectSchema { param( - [Parameter(ValueFromPipeline = $true)] + [Parameter( Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [hashtable] $Content, - [Parameter(ValueFromPipeline = $false)] + [Parameter()] [switch] $Properties, @@ -39,163 +39,195 @@ function ConvertTo-PodeOAObjectSchema { $DefinitionTag ) - - # Ensure all content types are valid MIME types - foreach ($type in $Content.Keys) { - if ($type -inotmatch '^(application|audio|image|message|model|multipart|text|video|\*)\/[\w\.\-\*]+(;[\s]*(charset|boundary)=[\w\.\-\*]+)*$|^"\*\/\*"$') { - throw "Invalid content-type found for schema: $($type)" - } + begin { + $pipelineItemCount = 0 # Initialize counter to track items in the pipeline. } - # manage generic schema json conversion issue - if ( $Content.ContainsKey('*/*')) { - $Content['"*/*"'] = $Content['*/*'] - $Content.Remove('*/*') + + process { + $pipelineItemCount++ # Increment the counter for each item in the pipeline. } - # convert each schema to OpenAPI format - # Initialize an empty hashtable for the schema - $obj = @{} - # Process each content type - $types = [string[]]$Content.Keys - foreach ($type in $types) { - # Initialize schema structure for the type - $obj[$type] = @{ } + end { + # Throw an error if more than one item is passed in the pipeline. + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } - # Handle upload content, array structures, and shared component schema references - if ($Content[$type].__upload) { - if ($Content[$type].__array) { - $upload = $Content[$type].__content.__upload - } - else { - $upload = $Content[$type].__upload + # Ensure all content types are valid MIME types. + foreach ($type in $Content.Keys) { + if ($type -inotmatch '^(application|audio|image|message|model|multipart|text|video|\*)\/[\w\.\-\*]+(;[\s]*(charset|boundary)=[\w\.\-\*]+)*$|^"\*\/\*"$') { + # Invalid content-type found for schema: $($type) + throw ($PodeLocale.invalidContentTypeForSchemaExceptionMessage -f $type) } + } - if ($type -ieq 'multipart/form-data' -and $upload.content ) { - if ((Test-PodeOAVersion -Version 3.1 -DefinitionTag $DefinitionTag ) -and $upload.partContentMediaType) { - foreach ($key in $upload.content.Properties ) { - if ($key.type -eq 'string' -and $key.format -and $key.format -ieq 'binary' -or $key.format -ieq 'base64') { - $key.ContentMediaType = $PartContentMediaType - $key.remove('format') - break - } - } + # Manage a specific case where a generic schema conversion issue may arise. + if ($Content.ContainsKey('*/*')) { + $Content['"*/*"'] = $Content['*/*'] # Adjust the key format for schema compatibility. + $Content.Remove('*/*') + } + + # Initialize an empty hashtable for the schema object. + $obj = [ordered]@{} + + # Get all the content keys (MIME types) to iterate through. + $types = [string[]]$Content.Keys + foreach ($type in $types) { + # Initialize schema structure for each type. + $obj[$type] = [ordered]@{} + + # Handle file upload content, arrays, and shared component schema references. + if ($Content[$type].__upload) { + # Check if the content is an array. + if ($Content[$type].__array) { + $upload = $Content[$type].__content.__upload } - $newContent = $upload.content - } - else { - if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $DefinitionTag) { - $newContent = [ordered]@{ - 'type' = 'string' - 'format' = $upload.contentEncoding + else { + $upload = $Content[$type].__upload + } + + # Handle specific multipart/form-data content processing. + if ($type -ieq 'multipart/form-data' -and $upload.content) { + if ((Test-PodeOAVersion -Version 3.1 -DefinitionTag $DefinitionTag) -and $upload.partContentMediaType) { + # Iterate through properties to set content media type and remove format for binaries. + foreach ($key in $upload.content.Properties) { + if ($key.type -eq 'string' -and ($key.format -ieq 'binary' -or $key.format -ieq 'base64')) { + $key.ContentMediaType = $PartContentMediaType + $key.remove('format') + break + } + } } + $newContent = $upload.content } else { - if ($ContentEncoding -ieq 'Base64') { + # Handle OpenAPI v3.0 specific content encoding. + if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $DefinitionTag) { $newContent = [ordered]@{ - 'type' = 'string' - 'contentEncoding' = $upload.contentEncoding + 'type' = 'string' + 'format' = $upload.contentEncoding } } + else { + # Handle Base64 content encoding. + if ($ContentEncoding -ieq 'Base64') { + $newContent = [ordered]@{ + 'type' = 'string' + 'contentEncoding' = $upload.contentEncoding + } + } + } + } + + # Update the content with the new encoding information. + if ($Content[$type].__array) { + $Content[$type].__content = $newContent + } + else { + $Content[$type] = $newContent } } + + # Process arrays and object properties based on content type. if ($Content[$type].__array) { - $Content[$type].__content = $newContent + $isArray = $true + $item = $Content[$type].__content + $obj[$type].schema = [ordered]@{ + 'type' = 'array' + 'items' = $null + } + # Include additional metadata if present. + if ($Content[$type].__title) { + $obj[$type].schema.title = $Content[$type].__title + } + if ($Content[$type].__uniqueItems) { + $obj[$type].schema.uniqueItems = $Content[$type].__uniqueItems + } + if ($Content[$type].__maxItems) { + $obj[$type].schema.__maxItems = $Content[$type].__maxItems + } + if ($Content[$type].minItems) { + $obj[$type].schema.minItems = $Content[$type].__minItems + } } else { - $Content[$type] = $newContent - } - } - - if ($Content[$type].__array) { - $isArray = $true - $item = $Content[$type].__content - $obj[$type].schema = [ordered]@{ - 'type' = 'array' - 'items' = $null - } - if ( $Content[$type].__title) { - $obj[$type].schema.title = $Content[$type].__title - } - if ( $Content[$type].__uniqueItems) { - $obj[$type].schema.uniqueItems = $Content[$type].__uniqueItems - } - if ( $Content[$type].__maxItems) { - $obj[$type].schema.__maxItems = $Content[$type].__maxItems - } - if ( $Content[$type].minItems) { - $obj[$type].schema.minItems = $Content[$type].__minItems - } - } - else { - $item = $Content[$type] - $isArray = $false - } - # Add set schema objects or empty content - if ($item -is [string]) { - if (![string]::IsNullOrEmpty($item )) { - #Check for empty reference - if (@('string', 'integer' , 'number', 'boolean' ) -icontains $item) { - if ($isArray) { - $obj[$type].schema.items = @{ - 'type' = $item.ToLower() + $item = $Content[$type] + $isArray = $false + } + + # Add schema objects or handle empty content. + if ($item -is [string]) { + if (![string]::IsNullOrEmpty($item)) { + # Handle basic type definitions or references. + if (@('string', 'integer', 'number', 'boolean') -icontains $item) { + if ($isArray) { + $obj[$type].schema.items = [ordered]@{ + 'type' = $item.ToLower() + } + } + else { + $obj[$type].schema = [ordered]@{ + 'type' = $item.ToLower() + } } } else { - $obj[$type].schema = @{ - 'type' = $item.ToLower() + # Handle component references. + Test-PodeOAComponentInternal -Field schemas -DefinitionTag $DefinitionTag -Name $item -PostValidation + if ($isArray) { + $obj[$type].schema.items = [ordered]@{ + '$ref' = "#/components/schemas/$($item)" + } + } + else { + $obj[$type].schema = [ordered]@{ + '$ref' = "#/components/schemas/$($item)" + } } } } else { - Test-PodeOAComponentInternal -Field schemas -DefinitionTag $DefinitionTag -Name $item -PostValidation - if ($isArray) { - $obj[$type].schema.items = @{ - '$ref' = "#/components/schemas/$($item)" - } - } - else { - $obj[$type].schema = @{ - '$ref' = "#/components/schemas/$($item)" - } - } + # Create an empty content entry. + $obj[$type] = [ordered]@{} } } else { - # Create an empty content - $obj[$type] = @{} - } - } - else { - if ($item.Count -eq 0) { - $result = @{} - } - else { - $result = ($item | ConvertTo-PodeOASchemaProperty -DefinitionTag $DefinitionTag) - } - if ($Properties) { - if ($item.Name) { - $obj[$type].schema = @{ - 'properties' = @{ - $item.Name = $result - } - } + if ($item.Count -eq 0) { + $result = [ordered]@{} # Create an empty object if the item count is zero. } else { - Throw 'The Properties parameters cannot be used if the Property has no name' + # Convert each property to a PodeOpenAPI schema property. + $result = ($item | ConvertTo-PodeOASchemaProperty -DefinitionTag $DefinitionTag) } - } - else { - if ($isArray) { - $obj[$type].schema.items = $result + + # Handle the Properties parameter case. + if ($Properties) { + if ($item.Name) { + $obj[$type].schema = [ordered]@{ + 'properties' = [ordered]@{ + $item.Name = $result + } + } + } + else { + # Throw an error if Properties parameter is used without a name. + throw ($PodeLocale.propertiesParameterWithoutNameExceptionMessage) + } } else { - $obj[$type].schema = $result + # Assign the resulting schema to the correct array or object location. + if ($isArray) { + $obj[$type].schema.items = $result + } + else { + $obj[$type].schema = $result + } } } } - } - return $obj + return $obj # Return the final OpenAPI schema object. + } } <# @@ -236,26 +268,26 @@ function Test-PodeOAComponentSchemaJson { <# .SYNOPSIS -Tests if a given name exists in the external path keys of OpenAPI definitions for specified definition tags. + Tests if a given name exists in the external path keys of OpenAPI definitions for specified definition tags. .DESCRIPTION -The Test-PodeOAComponentExternalPath function iterates over a list of definition tags and checks if a given name -is present in the external path keys of OpenAPI definitions within the Pode server context. This function is typically -used to validate if a specific component name is already defined in the external paths of the OpenAPI documentation. + The Test-PodeOAComponentExternalPath function iterates over a list of definition tags and checks if a given name + is present in the external path keys of OpenAPI definitions within the Pode server context. This function is typically + used to validate if a specific component name is already defined in the external paths of the OpenAPI documentation. .PARAMETER Name -The name of the external path component to be checked within the OpenAPI definitions. + The name of the external path component to be checked within the OpenAPI definitions. .PARAMETER DefinitionTag -An array of definition tags against which the existence of the name will be checked in the OpenAPI definitions. + An array of definition tags against which the existence of the name will be checked in the OpenAPI definitions. .EXAMPLE -$exists = Test-PodeOAComponentExternalPath -Name 'MyComponentName' -DefinitionTag @('tag1', 'tag2') + $exists = Test-PodeOAComponentExternalPath -Name 'MyComponentName' -DefinitionTag @('tag1', 'tag2') -Checks if 'MyComponentName' exists in the external path keys of OpenAPI definitions for 'tag1' and 'tag2'. + Checks if 'MyComponentName' exists in the external path keys of OpenAPI definitions for 'tag1' and 'tag2'. .NOTES -This is an internal function and may change in future releases of Pode. + This is an internal function and may change in future releases of Pode. #> function Test-PodeOAComponentExternalPath { param( @@ -284,27 +316,27 @@ function Test-PodeOAComponentExternalPath { <# .SYNOPSIS -Converts a property into an OpenAPI 'Of' property structure based on a given definition tag. + Converts a property into an OpenAPI 'Of' property structure based on a given definition tag. .DESCRIPTION -The ConvertTo-PodeOAOfProperty function is used to convert a given property into one of the OpenAPI 'Of' properties: -allOf, oneOf, or anyOf. These structures are used in OpenAPI documentation to define complex types. The function -constructs the appropriate structure based on the type of the property and the definition tag provided. + The ConvertTo-PodeOAOfProperty function is used to convert a given property into one of the OpenAPI 'Of' properties: + allOf, oneOf, or anyOf. These structures are used in OpenAPI documentation to define complex types. The function + constructs the appropriate structure based on the type of the property and the definition tag provided. .PARAMETER Property -A hashtable representing the property to be converted. It should contain the type (allOf, oneOf, or anyOf) and -potentially a list of schemas. + A hashtable representing the property to be converted. It should contain the type (allOf, oneOf, or anyOf) and + potentially a list of schemas. .PARAMETER DefinitionTag -A mandatory string parameter specifying the definition tag in OpenAPI documentation, used for validating components. + A mandatory string parameter specifying the definition tag in OpenAPI documentation, used for validating components. .EXAMPLE -$ofProperty = ConvertTo-PodeOAOfProperty -Property $myProperty -DefinitionTag 'myTag' + $ofProperty = ConvertTo-PodeOAOfProperty -Property $myProperty -DefinitionTag 'myTag' -Converts a given property into an OpenAPI 'Of' structure using the specified definition tag. + Converts a given property into an OpenAPI 'Of' structure using the specified definition tag. .NOTES -This is an internal function and may change in future releases of Pode. + This is an internal function and may change in future releases of Pode. #> function ConvertTo-PodeOAOfProperty { param ( @@ -320,10 +352,21 @@ function ConvertTo-PodeOAOfProperty { if (@('allOf', 'oneOf', 'anyOf') -inotcontains $Property.type) { return @{} } - # Initialize the schema with the 'Of' type - $schema = [ordered]@{ - $Property.type = @() + if ($Property.name) { + $schema = [ordered]@{ + $Property.name = [ordered]@{ + $Property.type = @() + } + } + if ($Property.description) { + $schema[$Property.name].description = $Property.description + } + } + else { + $schema = [ordered]@{ + $Property.type = @() + } } # Process each schema defined in the property @@ -332,11 +375,21 @@ function ConvertTo-PodeOAOfProperty { if ($prop -is [string]) { # Validate the schema component and add a reference to it Test-PodeOAComponentInternal -Field schemas -DefinitionTag $DefinitionTag -Name $prop -PostValidation - $schema[$Property.type] += @{ '$ref' = "#/components/schemas/$prop" } + if ($Property.name) { + $schema[$Property.name][$Property.type] += [ordered]@{ '$ref' = "#/components/schemas/$prop" } + } + else { + $schema[$Property.type] += [ordered]@{ '$ref' = "#/components/schemas/$prop" } + } } else { # Convert the property to an OpenAPI schema property - $schema[$Property.type] += $prop | ConvertTo-PodeOASchemaProperty -DefinitionTag $DefinitionTag + if ($Property.name) { + $schema[$Property.name][$Property.type] += $prop | ConvertTo-PodeOASchemaProperty -DefinitionTag $DefinitionTag + } + else { + $schema[$Property.type] += $prop | ConvertTo-PodeOASchemaProperty -DefinitionTag $DefinitionTag + } } } } @@ -350,10 +403,35 @@ function ConvertTo-PodeOAOfProperty { return $schema } +<# +.SYNOPSIS + Converts a hashtable representing a property into a schema property format compliant with the OpenAPI Specification (OAS). + +.DESCRIPTION + This function takes a hashtable input representing a property and converts it into a schema property format based on the OpenAPI Specification. + It handles various property types including primitives, arrays, and complex types with allOf, oneOf, anyOf constructs. + +.PARAMETER Property + A hashtable containing property details that need to be converted to an OAS schema property. + +.PARAMETER NoDescription + A switch parameter. If set, the description of the property will not be included in the output schema. + +.PARAMETER DefinitionTag + A mandatory string parameter specifying the definition context used for schema validation and compatibility checks with OpenAPI versions. + +.EXAMPLE + $propertyDetails = [ordered]@{ + type = 'string'; + description = 'A sample property'; + } + ConvertTo-PodeOASchemaProperty -Property $propertyDetails -DefinitionTag 'v1' + This example will convert a simple string property into an OpenAPI schema property. +#> function ConvertTo-PodeOASchemaProperty { param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [hashtable] $Property, @@ -364,247 +442,273 @@ function ConvertTo-PodeOASchemaProperty { [string] $DefinitionTag ) - - if ( @('allof', 'oneof', 'anyof') -icontains $Property.type) { - $schema = ConvertTo-PodeOAofProperty -DefinitionTag $DefinitionTag -Property $Property - } - else { - # base schema type - $schema = [ordered]@{ } - if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $DefinitionTag ) { - if ($Property.type -is [string[]]) { - throw 'Multi type properties requeired OpenApi Version 3.1 or above' - } - $schema['type'] = $Property.type.ToLower() - } - else { - $schema.type = @($Property.type.ToLower()) - if ($Property.nullable) { - $schema.type += 'null' - } - } - } - - if ($Property.externalDocs) { - $schema['externalDocs'] = $Property.externalDocs - } - - if (!$NoDescription -and $Property.description) { - $schema['description'] = $Property.description - } - - if ($Property.default) { - $schema['default'] = $Property.default + begin { + $pipelineItemCount = 0 } - if ($Property.deprecated) { - $schema['deprecated'] = $Property.deprecated - } - if ($Property.nullable -and (Test-PodeOAVersion -Version 3.0 -DefinitionTag $DefinitionTag )) { - $schema['nullable'] = $Property.nullable - } + process { - if ($Property.writeOnly) { - $schema['writeOnly'] = $Property.writeOnly + $pipelineItemCount++ } - if ($Property.readOnly) { - $schema['readOnly'] = $Property.readOnly - } + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } - if ($Property.example) { - if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $DefinitionTag ) { - $schema['example'] = $Property.example + if ( @('allof', 'oneof', 'anyof') -icontains $Property.type) { + $schema = ConvertTo-PodeOAofProperty -DefinitionTag $DefinitionTag -Property $Property } else { - if ($Property.example -is [Array]) { - $schema['examples'] = $Property.example + # base schema type + $schema = [ordered]@{ } + if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $DefinitionTag ) { + if ($Property.type -is [string[]]) { + # Multi type properties requeired OpenApi Version 3.1 or above + throw ($PodeLocale.multiTypePropertiesRequireOpenApi31ExceptionMessage) + } + $schema['type'] = $Property.type.ToLower() } else { - $schema['examples'] = @( $Property.example) + $schema.type = @($Property.type.ToLower()) + if ($Property.nullable) { + $schema.type += 'null' + } } } - } - if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $DefinitionTag ) { - if ($Property.minimum) { - $schema['minimum'] = $Property.minimum + + if ($Property.externalDocs) { + $schema['externalDocs'] = $Property.externalDocs } - if ($Property.maximum) { - $schema['maximum'] = $Property.maximum + if (!$NoDescription -and $Property.description) { + $schema['description'] = $Property.description } - if ($Property.exclusiveMaximum) { - $schema['exclusiveMaximum'] = $Property.exclusiveMaximum + if ($Property.default) { + $schema['default'] = $Property.default } - if ($Property.exclusiveMinimum) { - $schema['exclusiveMinimum'] = $Property.exclusiveMinimum + if ($Property.deprecated) { + $schema['deprecated'] = $Property.deprecated } - } - else { - if ($Property.maximum) { - if ($Property.exclusiveMaximum) { - $schema['exclusiveMaximum'] = $Property.maximum + if ($Property.nullable -and (Test-PodeOAVersion -Version 3.0 -DefinitionTag $DefinitionTag )) { + $schema['nullable'] = $Property.nullable + } + + if ($Property.writeOnly) { + $schema['writeOnly'] = $Property.writeOnly + } + + if ($Property.readOnly) { + $schema['readOnly'] = $Property.readOnly + } + + if ($Property.example) { + if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $DefinitionTag ) { + $schema['example'] = $Property.example } else { - $schema['maximum'] = $Property.maximum + if ($Property.example -is [Array]) { + $schema['examples'] = $Property.example + } + else { + $schema['examples'] = @( $Property.example) + } } } - if ($Property.minimum) { - if ($Property.exclusiveMinimum) { - $schema['exclusiveMinimum'] = $Property.minimum - } - else { + if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $DefinitionTag ) { + if ($Property.ContainsKey('minimum')) { $schema['minimum'] = $Property.minimum } - } - } - if ($Property.multipleOf) { - $schema['multipleOf'] = $Property.multipleOf - } - - if ($Property.pattern) { - $schema['pattern'] = $Property.pattern - } - - if ($Property.minLength) { - $schema['minLength'] = $Property.minLength - } - if ($Property.maxLength) { - $schema['maxLength'] = $Property.maxLength - } + if ($Property.ContainsKey('maximum')) { + $schema['maximum'] = $Property.maximum + } - if ($Property.xml ) { - $schema['xml'] = $Property.xml - } + if ($Property.exclusiveMaximum) { + $schema['exclusiveMaximum'] = $Property.exclusiveMaximum + } - if (Test-PodeOAVersion -Version 3.1 -DefinitionTag $DefinitionTag ) { - if ($Property.ContentMediaType) { - $schema['contentMediaType'] = $Property.ContentMediaType + if ($Property.exclusiveMinimum) { + $schema['exclusiveMinimum'] = $Property.exclusiveMinimum + } } - if ($Property.ContentEncoding) { - $schema['contentEncoding'] = $Property.ContentEncoding + else { + if ($Property.ContainsKey('maximum')) { + if ($Property.exclusiveMaximum) { + $schema['exclusiveMaximum'] = $Property.maximum + } + else { + $schema['maximum'] = $Property.maximum + } + } + if ($Property.ContainsKey('minimum')) { + if ($Property.exclusiveMinimum) { + $schema['exclusiveMinimum'] = $Property.minimum + } + else { + $schema['minimum'] = $Property.minimum + } + } + } + if ($Property.multipleOf) { + $schema['multipleOf'] = $Property.multipleOf } - } - # are we using an array? - if ($Property.array) { - if ($Property.maxItems ) { - $schema['maxItems'] = $Property.maxItems + if ($Property.pattern) { + $schema['pattern'] = $Property.pattern } - if ($Property.minItems ) { - $schema['minItems'] = $Property.minItems + if ($Property.ContainsKey('minLength')) { + $schema['minLength'] = $Property.minLength } - if ($Property.uniqueItems ) { - $schema['uniqueItems'] = $Property.uniqueItems + if ($Property.ContainsKey('maxLength')) { + $schema['maxLength'] = $Property.maxLength } - $schema['type'] = 'array' - if ($Property.type -ieq 'schema') { - Test-PodeOAComponentInternal -Field schemas -DefinitionTag $DefinitionTag -Name $Property['schema'] -PostValidation - $schema['items'] = @{ '$ref' = "#/components/schemas/$($Property['schema'])" } + if ($Property.xml ) { + $schema['xml'] = $Property.xml } - else { - $Property.array = $false - if ($Property.xml) { - $xmlFromProperties = $Property.xml - $Property.Remove('xml') - } - $schema['items'] = ($Property | ConvertTo-PodeOASchemaProperty -DefinitionTag $DefinitionTag) - $Property.array = $true - if ($xmlFromProperties) { - $Property.xml = $xmlFromProperties - } - if ($Property.xmlItemName) { - $schema.items.xml = @{'name' = $Property.xmlItemName } + if (Test-PodeOAVersion -Version 3.1 -DefinitionTag $DefinitionTag ) { + if ($Property.ContentMediaType) { + $schema['contentMediaType'] = $Property.ContentMediaType + } + if ($Property.ContentEncoding) { + $schema['contentEncoding'] = $Property.ContentEncoding } - } - return $schema - } - else { - #format is not applicable to array - if ($Property.format) { - $schema['format'] = $Property.format } - # schema refs - if ($Property.type -ieq 'schema') { - Test-PodeOAComponentInternal -Field schemas -DefinitionTag $DefinitionTag -Name $Property['schema'] -PostValidation - $schema = @{ - '$ref' = "#/components/schemas/$($Property['schema'])" + # are we using an array? + if ($Property.array) { + if ($Property.ContainsKey('maxItems') ) { + $schema['maxItems'] = $Property.maxItems } - } - #only if it's not an array - if ($Property.enum ) { - $schema['enum'] = $Property.enum - } - } - if ($Property.object) { - # are we using an object? - $Property.object = $false + if ($Property.ContainsKey('minItems') ) { + $schema['minItems'] = $Property.minItems + } - $schema = @{ - type = 'object' - properties = (ConvertTo-PodeOASchemaObjectProperty -DefinitionTag $DefinitionTag -Properties $Property) - } - $Property.object = $true - if ($Property.required) { - $schema['required'] = @($Property.name) - } - } + if ($Property.uniqueItems ) { + $schema['uniqueItems'] = $Property.uniqueItems + } - if ($Property.type -ieq 'object') { - foreach ($prop in $Property.properties) { - if ( @('allOf', 'oneOf', 'anyOf') -icontains $prop.type) { - switch ($prop.type.ToLower()) { - 'allof' { $prop.type = 'allOf' } - 'oneof' { $prop.type = 'oneOf' } - 'anyof' { $prop.type = 'anyOf' } + $schema['type'] = 'array' + if ($Property.type -ieq 'schema') { + Test-PodeOAComponentInternal -Field schemas -DefinitionTag $DefinitionTag -Name $Property['schema'] -PostValidation + $schema['items'] = [ordered]@{ '$ref' = "#/components/schemas/$($Property['schema'])" } + } + else { + $Property.array = $false + if ($Property.xml) { + $xmlFromProperties = $Property.xml + $Property.Remove('xml') + } + $schema['items'] = ($Property | ConvertTo-PodeOASchemaProperty -DefinitionTag $DefinitionTag) + $Property.array = $true + if ($xmlFromProperties) { + $Property.xml = $xmlFromProperties } - $schema += ConvertTo-PodeOAofProperty -DefinitionTag $DefinitionTag -Property $prop + if ($Property.xmlItemName) { + $schema.items.xml = [ordered]@{'name' = $Property.xmlItemName } + } } - } - if ($Property.properties) { - $schema['properties'] = (ConvertTo-PodeOASchemaObjectProperty -DefinitionTag $DefinitionTag -Properties $Property.properties) - $RequiredList = @(($Property.properties | Where-Object { $_.required }) ) - if ( $RequiredList.Count -gt 0) { - $schema['required'] = @($RequiredList.name) - } + return $schema } else { - #if noproperties parameter create an empty properties - if ( $Property.properties.Count -eq 1 -and $null -eq $Property.properties[0]) { - $schema['properties'] = @{} + #format is not applicable to array + if ($Property.format) { + $schema['format'] = $Property.format + } + + # schema refs + if ($Property.type -ieq 'schema') { + Test-PodeOAComponentInternal -Field schemas -DefinitionTag $DefinitionTag -Name $Property['schema'] -PostValidation + $schema = [ordered]@{ + '$ref' = "#/components/schemas/$($Property['schema'])" + } + } + #only if it's not an array + if ($Property.enum ) { + $schema['enum'] = $Property.enum } } + if ($Property.object) { + # are we using an object? + $Property.object = $false - if ($Property.minProperties) { - $schema['minProperties'] = $Property.minProperties + $schema = [ordered]@{ + type = 'object' + properties = (ConvertTo-PodeOASchemaObjectProperty -DefinitionTag $DefinitionTag -Properties $Property) + } + $Property.object = $true + if ($Property.required) { + $schema['required'] = @($Property.name) + } } - if ($Property.maxProperties) { - $schema['maxProperties'] = $Property.maxProperties - } + if ($Property.type -ieq 'object') { + $schema['properties'] = [ordered]@{} + foreach ($prop in $Property.properties) { + if ( @('allOf', 'oneOf', 'anyOf') -icontains $prop.type) { + switch ($prop.type.ToLower()) { + 'allof' { $prop.type = 'allOf' } + 'oneof' { $prop.type = 'oneOf' } + 'anyof' { $prop.type = 'anyOf' } + } + if ($prop.name) { + $schema['properties'] += ConvertTo-PodeOAofProperty -DefinitionTag $DefinitionTag -Property $prop + } + else { + $schema += ConvertTo-PodeOAofProperty -DefinitionTag $DefinitionTag -Property $prop + } - if ($Property.additionalProperties) { - $schema['additionalProperties'] = $Property.additionalProperties - } + } + } + if ($Property.properties) { + $schema['properties'] = (ConvertTo-PodeOASchemaObjectProperty -DefinitionTag $DefinitionTag -Properties $Property.properties) + $RequiredList = @(($Property.properties | Where-Object { $_.required }) ) + if ( $RequiredList.Count -gt 0) { + $schema['required'] = @($RequiredList.name) + } + } + else { + #if noproperties parameter create an empty properties + if ( $Property.properties.Count -eq 1 -and $null -eq $Property.properties[0]) { + $schema['properties'] = @{} + } + } - if ($Property.discriminator) { - $schema['discriminator'] = $Property.discriminator - } - } - return $schema + if ($Property.minProperties) { + $schema['minProperties'] = $Property.minProperties + } + + if ($Property.maxProperties) { + $schema['maxProperties'] = $Property.maxProperties + } + #Fix an issue when additionalProperties has an assigned value of $false + if ($Property.ContainsKey('additionalProperties')) { + if ($Property.additionalProperties) { + $schema['additionalProperties'] = $Property.additionalProperties | ConvertTo-PodeOASchemaProperty -DefinitionTag $DefinitionTag + } + else { + #the value is $false + $schema['additionalProperties'] = $false + } + } + if ($Property.discriminator) { + $schema['discriminator'] = $Property.discriminator + } + } + + return $schema + } } <# @@ -643,7 +747,7 @@ function ConvertTo-PodeOASchemaObjectProperty { ) # Initialize an empty hashtable for the schema - $schema = @{} + $schema = [ordered]@{} # Iterate over each property and convert to OpenAPI schema property if applicable foreach ($prop in $Properties) { @@ -733,14 +837,14 @@ function Set-PodeOpenApiRouteValue { #if scope is empty means 'any role' => assign an empty array $sctValue = @() } - $pm.security += @{ $sct = $sctValue } + $pm.security += [ordered]@{ $sct = $sctValue } } elseif ($sct -eq '%_allowanon_%') { #allow anonymous access - $pm.security += @{} + $pm.security += [ordered]@{} } else { - $pm.security += @{$sct = @() } + $pm.security += [ordered]@{$sct = @() } } } } @@ -749,7 +853,7 @@ function Set-PodeOpenApiRouteValue { } else { # Set responses or default to '204 No Content' if not specified - $pm.responses = @{'204' = @{'description' = (Get-PodeStatusDescription -StatusCode 204) } } + $pm.responses = [ordered]@{'204' = [ordered]@{'description' = (Get-PodeStatusDescription -StatusCode 204) } } } # Return the processed route properties return $pm @@ -777,7 +881,7 @@ Mandatory. A tag that identifies the specific OpenAPI definition to be generated Ordered dictionary representing the OpenAPI definition, which can be further processed into JSON or YAML format. .EXAMPLE -$metaInfo = @{ +$metaInfo = [ordered]@{ Title = "My API"; Version = "v1"; Description = "This is my API description." @@ -806,7 +910,8 @@ function Get-PodeOpenApiDefinitionInternal { $Definition = $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag] if (!$Definition.Version) { - throw 'OpenApi openapi field is required' + # OpenApi Version property is mandatory + throw ($PodeLocale.openApiVersionPropertyMandatoryExceptionMessage) } $localEndpoint = $null # set the openapi version @@ -862,13 +967,14 @@ function Get-PodeOpenApiDefinitionInternal { $def['paths'] = [ordered]@{} if ($Definition.webhooks.count -gt 0) { if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $DefinitionTag) { - throw 'Feature webhooks is unsupported in OpenAPI v3.0.x' + # Webhooks feature is unsupported in OpenAPI v3.0.x + throw ($PodeLocale.webhooksFeatureNotSupportedInOpenApi30ExceptionMessage) } else { $keys = [string[]]$Definition.webhooks.Keys foreach ($key in $keys) { if ($Definition.webhooks[$key].NotPrepared) { - $Definition.webhooks[$key] = @{ + $Definition.webhooks[$key] = [ordered]@{ $Definition.webhooks[$key].Method = Set-PodeOpenApiRouteValue -Route $Definition.webhooks[$key] -DefinitionTag $DefinitionTag } } @@ -909,13 +1015,14 @@ function Get-PodeOpenApiDefinitionInternal { } if ($components.pathItems.count -gt 0) { if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $DefinitionTag) { - throw 'Feature pathItems is unsupported in OpenAPI v3.0.x' + # Feature pathItems is unsupported in OpenAPI v3.0.x + throw ($PodeLocale.pathItemsFeatureNotSupportedInOpenApi30ExceptionMessage) } else { $keys = [string[]]$components.pathItems.Keys foreach ($key in $keys) { if ($components.pathItems[$key].NotPrepared) { - $components.pathItems[$key] = @{ + $components.pathItems[$key] = [ordered]@{ $components.pathItems[$key].Method = Set-PodeOpenApiRouteValue -Route $components.pathItems[$key] -DefinitionTag $DefinitionTag } } @@ -926,16 +1033,13 @@ function Get-PodeOpenApiDefinitionInternal { # auth/security components if ($PodeContext.Server.Authentications.Methods.Count -gt 0) { - #if ($null -eq $def.components.securitySchemes) { - # $def.components.securitySchemes = @{} - # } $authNames = (Expand-PodeAuthMerge -Names $PodeContext.Server.Authentications.Methods.Keys) foreach ($authName in $authNames) { $authType = (Find-PodeAuth -Name $authName).Scheme $_authName = ($authName -replace '\s+', '') - $_authObj = @{} + $_authObj = [ordered]@{} if ($authType.Scheme -ieq 'apikey') { $_authObj = [ordered]@{ @@ -967,7 +1071,7 @@ function Get-PodeOpenApiDefinitionInternal { if ($authType.Arguments.Description) { $_authObj.description = $authType.Arguments.Description } - $_authObj.flows = @{ + $_authObj.flows = [ordered]@{ $oAuthFlow = [ordered]@{ } } @@ -982,7 +1086,7 @@ function Get-PodeOpenApiDefinitionInternal { $_authObj.flows.$oAuthFlow.refreshUrl = $authType.Arguments.Urls.Refresh } - $_authObj.flows.$oAuthFlow.scopes = @{} + $_authObj.flows.$oAuthFlow.scopes = [ordered]@{} if ($authType.Arguments.Scopes ) { foreach ($scope in $authType.Arguments.Scopes) { if ($PodeContext.Server.Authorisations.Methods.ContainsKey($scope) -and $PodeContext.Server.Authorisations.Methods[$scope].Scheme.Type -ieq 'Scope' -and $PodeContext.Server.Authorisations.Methods[$scope].Description) { @@ -995,7 +1099,7 @@ function Get-PodeOpenApiDefinitionInternal { } } else { - $_authObj = @{ + $_authObj = [ordered]@{ type = $authType.Scheme.ToLowerInvariant() scheme = $authType.Name.ToLowerInvariant() } @@ -1054,11 +1158,16 @@ function Get-PodeOpenApiDefinitionInternal { # add path to defintion if ($null -eq $def.paths[$_route.OpenApi.Path]) { - $def.paths[$_route.OpenApi.Path] = @{} + $def.paths[$_route.OpenApi.Path] = [ordered]@{} } # add path's http method to defintition $pm = Set-PodeOpenApiRouteValue -Route $_route -DefinitionTag $DefinitionTag + if ($pm.responses.Count -eq 0) { + $pm.responses += [ordered]@{ + 'default' = [ordered]@{'description' = 'No description' } + } + } $def.paths[$_route.OpenApi.Path][$method] = $pm # add any custom server endpoints for route @@ -1080,12 +1189,12 @@ function Get-PodeOpenApiDefinitionInternal { $serverDef = $null if (![string]::IsNullOrWhiteSpace($_route.Endpoint.Name)) { - $serverDef = @{ + $serverDef = [ordered]@{ url = (Get-PodeEndpointByName -Name $_route.Endpoint.Name).Url } } else { - $serverDef = @{ + $serverDef = [ordered]@{ url = "$($_route.Endpoint.Protocol)://$($_route.Endpoint.Address)" } } @@ -1105,63 +1214,82 @@ function Get-PodeOpenApiDefinitionInternal { foreach ($method in $extPath.keys) { $_route = $extPath[$method] if (! ( $def.paths.keys -ccontains $_route.Path)) { - $def.paths[$_route.OpenAPI.Path] = @{} + $def.paths[$_route.OpenAPI.Path] = [ordered]@{} } $pm = Set-PodeOpenApiRouteValue -Route $_route -DefinitionTag $DefinitionTag # add path's http method to defintition - $def.paths[$_route.OpenAPI.Path][$method.ToLower()] = $pmF + $def.paths[$_route.OpenAPI.Path][$method.ToLower()] = $pm } } } return $def } +<# +.SYNOPSIS + Converts a cmdlet parameter to a Pode OpenAPI property. + +.DESCRIPTION + This internal function takes a cmdlet parameter and converts it into an appropriate Pode OpenAPI property based on its type. + The function supports boolean, integer, float, and string parameter types. + +.PARAMETER Parameter + The cmdlet parameter metadata that needs to be converted. This parameter is mandatory and accepts values from the pipeline. + +.EXAMPLE + $metadata = Get-Command -Name Get-Process | Select-Object -ExpandProperty Parameters + $metadata.Values | ConvertTo-PodeOAPropertyFromCmdletParameter + +.NOTES + This is an internal function and may change in future releases of Pode. +#> function ConvertTo-PodeOAPropertyFromCmdletParameter { param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [System.Management.Automation.ParameterMetadata] $Parameter ) + process { + if ($Parameter.SwitchParameter -or ($Parameter.ParameterType.Name -ieq 'boolean')) { + New-PodeOABoolProperty -Name $Parameter.Name + } + else { + switch ($Parameter.ParameterType.Name) { + { @('int32', 'int64') -icontains $_ } { + New-PodeOAIntProperty -Name $Parameter.Name -Format $_ + } - if ($Parameter.SwitchParameter -or ($Parameter.ParameterType.Name -ieq 'boolean')) { - New-PodeOABoolProperty -Name $Parameter.Name - } - else { - switch ($Parameter.ParameterType.Name) { - { @('int32', 'int64') -icontains $_ } { - New-PodeOAIntProperty -Name $Parameter.Name -Format $_ - } - - { @('double', 'float') -icontains $_ } { - New-PodeOANumberProperty -Name $Parameter.Name -Format $_ + { @('double', 'float') -icontains $_ } { + New-PodeOANumberProperty -Name $Parameter.Name -Format $_ + } } } - } - New-PodeOAStringProperty -Name $Parameter.Name + New-PodeOAStringProperty -Name $Parameter.Name + } } <# .SYNOPSIS -Creates a base OpenAPI object structure. + Creates a base OpenAPI object structure. .DESCRIPTION -The Get-PodeOABaseObject function generates a foundational structure for an OpenAPI object. -This structure includes empty ordered dictionaries for info, paths, webhooks, components, and other OpenAPI elements. -It is used as a base template for building OpenAPI documentation in the Pode framework. + The Get-PodeOABaseObject function generates a foundational structure for an OpenAPI object. + This structure includes empty ordered dictionaries for info, paths, webhooks, components, and other OpenAPI elements. + It is used as a base template for building OpenAPI documentation in the Pode framework. .OUTPUTS -Hashtable -Returns a hashtable representing the base structure of an OpenAPI object. + Hashtable + Returns a hashtable representing the base structure of an OpenAPI object. .EXAMPLE -$baseObject = Get-PodeOABaseObject + $baseObject = Get-PodeOABaseObject -This example creates a base OpenAPI object structure. + This example creates a base OpenAPI object structure. .NOTES -This is an internal function and may change in future releases of Pode. + This is an internal function and may change in future releases of Pode. #> function Get-PodeOABaseObject { # Returns a base template for an OpenAPI object @@ -1191,21 +1319,21 @@ function Get-PodeOABaseObject { schemaJson = @{} viewer = @{} postValidation = @{ - schemas = @{} - responses = @{} - parameters = @{} - examples = @{} - requestBodies = @{} - headers = @{} - securitySchemes = @{} - links = @{} - callbacks = @{} - pathItems = @{} + schemas = [ordered]@{} + responses = [ordered]@{} + parameters = [ordered]@{} + examples = [ordered]@{} + requestBodies = [ordered]@{} + headers = [ordered]@{} + securitySchemes = [ordered]@{} + links = [ordered]@{} + callbacks = [ordered]@{} + pathItems = [ordered]@{} } externalPath = [ordered]@{} - defaultResponses = @{ - '200' = @{ description = 'OK' } - 'default' = @{ description = 'Internal server error' } + defaultResponses = [ordered]@{ + '200' = [ordered]@{ description = 'OK' } + 'default' = [ordered]@{ description = 'Internal server error' } } operationId = @() } @@ -1244,22 +1372,15 @@ This is an internal function and may change in future releases of Pode. function Initialize-PodeOpenApiTable { param( [string] - $DefaultDefinitionTag = $null + $DefaultDefinitionTag = 'default' ) # Initialization of the OpenAPI table with default settings $OpenAPI = @{ - DefinitionTagSelectionStack = New-Object 'System.Collections.Generic.Stack[System.Object]' - } - # Set the default definition tag - if ([string]::IsNullOrEmpty($DefaultDefinitionTag)) { - $OpenAPI['DefaultDefinitionTag'] = 'default' - } - else { - $OpenAPI['DefaultDefinitionTag'] = $DefaultDefinitionTag + DefinitionTagSelectionStack = [System.Collections.Generic.Stack[System.Object]]::new() } # Set the currently selected definition tag - $OpenAPI['SelectedDefinitionTag'] = $OpenAPI['DefaultDefinitionTag'] + $OpenAPI['SelectedDefinitionTag'] = $DefaultDefinitionTag # Initialize the Definitions dictionary with a base OpenAPI object for the selected definition tag $OpenAPI['Definitions'] = @{ $OpenAPI['SelectedDefinitionTag'] = Get-PodeOABaseObject } @@ -1313,7 +1434,7 @@ function Set-PodeOAAuth { # Validate the existence of specified authentication methods foreach ($n in @($Name)) { if (!(Test-PodeAuthExists -Name $n)) { - throw "Authentication method does not exist: $($n)" + throw ($PodeLocale.authenticationMethodDoesNotExistExceptionMessage -f $n) #"Authentication method does not exist: $($n)" } } } @@ -1331,7 +1452,7 @@ function Set-PodeOAAuth { }) # Add anonymous access if allowed if ($AllowAnon) { - $r.OpenApi.Authentication += @{'%_allowanon_%' = '' } + $r.OpenApi.Authentication += [ordered]@{'%_allowanon_%' = '' } } } } @@ -1380,7 +1501,7 @@ function Set-PodeOAGlobalAuth { # Check if the specified authentication method exists if (!(Test-PodeAuthExists -Name $Name)) { - throw "Authentication method does not exist: $($Name)" + throw ($PodeLocale.authenticationMethodDoesNotExistExceptionMessage -f $Name) #"Authentication method does not exist: $($Name)" } # Iterate over each definition tag to apply the authentication method @@ -1403,14 +1524,44 @@ function Set-PodeOAGlobalAuth { } # Update the OpenAPI definition with the authentication information - $PodeContext.Server.OpenAPI.Definitions[$tag].Security += @{ - Definition = @{ "$($authName -replace '\s+', '')" = $Scopes } + $PodeContext.Server.OpenAPI.Definitions[$tag].Security += [ordered]@{ + Definition = [ordered]@{ "$($authName -replace '\s+', '')" = $Scopes } Route = (ConvertTo-PodeRouteRegex -Path $Route) } } } } +<# +.SYNOPSIS + Resolves references in an OpenAPI schema component based on definitions within a specified definition tag context. + +.DESCRIPTION + This function navigates through a schema's properties and resolves `$ref` references to actual schemas defined within the specified definition context. + It handles complex constructs such as 'allOf', 'oneOf', and 'anyOf', merging properties and ensuring the schema is fully resolved without unresolved references. + +.PARAMETER ComponentSchema + A hashtable representing the schema of a component where references need to be resolved. + +.PARAMETER DefinitionTag + A string identifier for the specific set of schema definitions under which references should be resolved. + +.EXAMPLE + $schema = [ordered]@{ + type = 'object'; + properties = [ordered]@{ + name = [ordered]@{ + type = 'string' + }; + details = [ordered]@{ + '$ref' = '#/components/schemas/UserDetails' + } + }; + } + Resolve-PodeOAReference -ComponentSchema $schema -DefinitionTag 'v1' + + This example demonstrates resolving a reference to 'UserDetails' within a given component schema. +#> function Resolve-PodeOAReference { param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] @@ -1423,11 +1574,13 @@ function Resolve-PodeOAReference { ) begin { + # Initialize schema storage and a list to track keys that need resolution $Schemas = $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.schemaJson $Keys = @() } process { + # Gather all keys from properties and directly from the schema that might have references if ($ComponentSchema.properties) { foreach ($item in $ComponentSchema.properties.Keys) { $Keys += $item @@ -1439,49 +1592,62 @@ function Resolve-PodeOAReference { } } + # Process each key to resolve references or merge schema definitions foreach ($key in $Keys) { if ( @('allof', 'oneof', 'anyof') -icontains $key ) { - if ($key -ieq 'allof') { - $tmpProp = @() - foreach ( $comp in $ComponentSchema[$key] ) { - if ($comp.'$ref') { - if (($comp.'$ref').StartsWith('#/components/schemas/')) { - $refName = ($comp.'$ref') -replace '#/components/schemas/', '' - if ($Schemas.ContainsKey($refName)) { - $tmpProp += $Schemas[$refName].schema + # Handle complex schema constructs like allOf, oneOf, and anyOf + switch ($key.ToLower()) { + 'allof' { + $tmpProp = @() + foreach ( $comp in $ComponentSchema[$key] ) { + if ($comp.'$ref') { + # Resolve $ref to a schema if it starts with the expected path + if (($comp.'$ref').StartsWith('#/components/schemas/')) { + $refName = ($comp.'$ref') -replace '#/components/schemas/', '' + if ($Schemas.ContainsKey($refName)) { + $tmpProp += $Schemas[$refName].schema + } } } - } - elseif ( $comp.properties) { - if ($comp.type -eq 'object') { - $tmpProp += Resolve-PodeOAReference -DefinitionTag $DefinitionTag -ComponentSchema$comp + elseif ( $comp.properties) { + # Recursively resolve nested schemas + if ($comp.type -eq 'object') { + $tmpProp += Resolve-PodeOAReference -DefinitionTag $DefinitionTag -ComponentSchema$comp + } + else { + # Unsupported object + throw ($PodeLocale.unsupportedObjectExceptionMessage) + } } - else { - throw 'Unsupported object' + } + # Update the main schema to be an object and add resolved properties + $ComponentSchema.type = 'object' + $ComponentSchema.remove('allOf') + if ($tmpProp.count -gt 0) { + foreach ($t in $tmpProp) { + $ComponentSchema.properties += $t.properties } } - } - $ComponentSchema.type = 'object' - $ComponentSchema.remove('allOf') - if ($tmpProp.count -gt 0) { - foreach ($t in $tmpProp) { - $ComponentSchema.properties += $t.properties - } } - - } - elseif ($key -ieq 'oneof') { - throw 'Validation of schema with oneof is not supported' - } - elseif ($key -ieq 'anyof') { - throw 'Validation of schema with anyof is not supported' + 'oneof' { + # Throw an error for unsupported schema constructs to notify the user + # Validation of schema with oneof is not supported + throw ($PodeLocale.validationOfOneOfSchemaNotSupportedExceptionMessage) + } + 'anyof' { + # Throw an error for unsupported schema constructs to notify the user + # Validation of schema with anyof is not supported + throw ($PodeLocale.validationOfAnyOfSchemaNotSupportedExceptionMessage) + } } } elseif ($ComponentSchema.properties[$key].type -eq 'object') { + # Recursively resolve object-type properties $ComponentSchema.properties[$key].properties = Resolve-PodeOAReference -DefinitionTag $DefinitionTag -ComponentSchema $ComponentSchema.properties[$key].properties } elseif ($ComponentSchema.properties[$key].'$ref') { + # Resolve property references within the main properties of the schema if (($ComponentSchema.properties[$key].'$ref').StartsWith('#/components/schemas/')) { $refName = ($ComponentSchema.properties[$key].'$ref') -replace '#/components/schemas/', '' if ($Schemas.ContainsKey($refName)) { @@ -1501,37 +1667,37 @@ function Resolve-PodeOAReference { } end { + # Return the fully resolved component schema return $ComponentSchema } } - <# .SYNOPSIS -Creates a new OpenAPI property object based on provided parameters. + Creates a new OpenAPI property object based on provided parameters. .DESCRIPTION -The New-PodeOAPropertyInternal function constructs an OpenAPI property object using parameters like type, name, -description, and various other attributes. It is used internally for building OpenAPI documentation elements in the Pode framework. + The New-PodeOAPropertyInternal function constructs an OpenAPI property object using parameters like type, name, + description, and various other attributes. It is used internally for building OpenAPI documentation elements in the Pode framework. .PARAMETER Type -The type of the property. This parameter is optional if the type is specified in the Params hashtable. + The type of the property. This parameter is optional if the type is specified in the Params hashtable. .PARAMETER Params -A hashtable containing various attributes of the property such as name, description, format, and constraints like -required, readOnly, writeOnly, etc. + A hashtable containing various attributes of the property such as name, description, format, and constraints like + required, readOnly, writeOnly, etc. .OUTPUTS -System.Collections.Specialized.OrderedDictionary -An ordered dictionary representing the constructed OpenAPI property object. + System.Collections.Specialized.OrderedDictionary + An ordered dictionary representing the constructed OpenAPI property object. .EXAMPLE -$property = New-PodeOAPropertyInternal -Type 'string' -Params $myParams + $property = New-PodeOAPropertyInternal -Type 'string' -Params $myParams -Demonstrates how to create an OpenAPI property object of type 'string' using the specified parameters. + Demonstrates how to create an OpenAPI property object of type 'string' using the specified parameters. .NOTES -This is an internal function and may change in future releases of Pode. + This is an internal function and may change in future releases of Pode. #> function New-PodeOAPropertyInternal { [OutputType([System.Collections.Specialized.OrderedDictionary])] @@ -1557,7 +1723,8 @@ function New-PodeOAPropertyInternal { $param.type = $Params.type } else { - throw 'Cannot create the property no type is defined' + # Cannot create the property no type is defined + throw ($PodeLocale.cannotCreatePropertyWithoutTypeExceptionMessage) } } @@ -1594,15 +1761,15 @@ function New-PodeOAPropertyInternal { if ($Params.UniqueItems.IsPresent) { $param.uniqueItems = $Params.UniqueItems.IsPresent } - if ($Params.MaxItems) { $param.maxItems = $Params.MaxItems } + if ($Params.ContainsKey('MaxItems')) { $param.maxItems = $Params.MaxItems } - if ($Params.MinItems) { $param.minItems = $Params.MinItems } + if ($Params.ContainsKey('MinItems')) { $param.minItems = $Params.MinItems } if ($Params.Enum) { $param.enum = $Params.Enum } - if ($Params.Minimum) { $param.minimum = $Params.Minimum } + if ($Params.ContainsKey('Minimum')) { $param.minimum = $Params.Minimum } - if ($Params.Maximum) { $param.maximum = $Params.Maximum } + if ($Params.ContainsKey('Maximum')) { $param.maximum = $Params.Maximum } if ($Params.ExclusiveMaximum.IsPresent) { $param.exclusiveMaximum = $Params.ExclusiveMaximum.IsPresent } @@ -1611,17 +1778,17 @@ function New-PodeOAPropertyInternal { if ($Params.Pattern) { $param.pattern = $Params.Pattern } - if ($Params.MinLength) { $param.minLength = $Params.MinLength } + if ($Params.ContainsKey('MinLength')) { $param.minLength = $Params.MinLength } - if ($Params.MaxLength) { $param.maxLength = $Params.MaxLength } + if ($Params.ContainsKey('MaxLength')) { $param.maxLength = $Params.MaxLength } - if ($Params.MinProperties) { $param.minProperties = $Params.MinProperties } + if ($Params.ContainsKey('MinProperties')) { $param.minProperties = $Params.MinProperties } - if ($Params.MaxProperties) { $param.maxProperties = $Params.MaxProperties } + if ($Params.ContainsKey('MaxProperties')) { $param.maxProperties = $Params.MaxProperties } if ($Params.XmlName -or $Params.XmlNamespace -or $Params.XmlPrefix -or $Params.XmlAttribute.IsPresent -or $Params.XmlWrapped.IsPresent) { - $param.xml = @{} + $param.xml = [ordered]@{} if ($Params.XmlName) { $param.xml.name = $Params.XmlName } @@ -1639,7 +1806,8 @@ function New-PodeOAPropertyInternal { if ($Params.ExternalDocs) { $param.externalDocs = $Params.ExternalDocs } if ($Params.NoAdditionalProperties.IsPresent -and $Params.AdditionalProperties) { - throw 'Params -NoAdditionalProperties and -AdditionalProperties are mutually exclusive' + # Parameters 'NoAdditionalProperties' and 'AdditionalProperties' are mutually exclusive + throw ($PodeLocale.parametersMutuallyExclusiveExceptionMessage -f 'NoAdditionalProperties', 'AdditionalProperties') } else { if ($Params.NoAdditionalProperties.IsPresent) { $param.additionalProperties = $false } @@ -1653,23 +1821,23 @@ function New-PodeOAPropertyInternal { <# .SYNOPSIS -Converts header properties to a format compliant with OpenAPI specifications. + Converts header properties to a format compliant with OpenAPI specifications. .DESCRIPTION -The ConvertTo-PodeOAHeaderProperty function is designed to take an array of hashtables representing header properties and -convert them into a structure suitable for OpenAPI documentation. It ensures that each header property includes a name and -schema definition and can handle additional attributes like description. + The ConvertTo-PodeOAHeaderProperty function is designed to take an array of hashtables representing header properties and + convert them into a structure suitable for OpenAPI documentation. It ensures that each header property includes a name and + schema definition and can handle additional attributes like description. .PARAMETER Headers -An array of hashtables, where each hashtable represents a header property with attributes like name, type, description, etc. + An array of hashtables, where each hashtable represents a header property with attributes like name, type, description, etc. .EXAMPLE -$headerProperties = ConvertTo-PodeOAHeaderProperty -Headers $myHeaders + $headerProperties = ConvertTo-PodeOAHeaderProperty -Headers $myHeaders -This example demonstrates how to convert an array of header properties into a format suitable for OpenAPI documentation. + This example demonstrates how to convert an array of header properties into a format suitable for OpenAPI documentation. .NOTES -This is an internal function and may change in future releases of Pode. + This is an internal function and may change in future releases of Pode. #> function ConvertTo-PodeOAHeaderProperty { param ( @@ -1678,58 +1846,75 @@ function ConvertTo-PodeOAHeaderProperty { $Headers ) - $elems = @{} + begin { + # Initialize an array to hold piped-in values + $pipelineValue = @() + $elems = [ordered]@{} + } - foreach ($e in $Headers) { - # Ensure each header has a name - if ($e.name) { - $elems.$($e.name) = @{} - # Add description if present - if ($e.description) { - $elems.$($e.name).description = $e.description - } - # Define the schema, including the type and any additional properties - $elems.$($e.name).schema = @{ - type = $($e.type) - } - foreach ($k in $e.keys) { - if (@('name', 'description') -notcontains $k) { - $elems.$($e.name).schema.$k = $e.$k + process { + # Add the current piped-in value to the array + $pipelineValue += $_ + } + + end { + # Set Headers to the array of values + if ($pipelineValue.Count -gt 1) { + $Headers = $pipelineValue + } + + foreach ($e in $Headers) { + # Ensure each header has a name + if ($e.name) { + $elems.$($e.name) = @{} + # Add description if present + if ($e.description) { + $elems.$($e.name).description = $e.description + } + # Define the schema, including the type and any additional properties + $elems.$($e.name).schema = @{ + type = $($e.type) + } + foreach ($k in $e.keys) { + if (@('name', 'description') -notcontains $k) { + $elems.$($e.name).schema.$k = $e.$k + } } } + else { + # Header requires a name when used in an encoding context + throw ($PodeLocale.headerMustHaveNameInEncodingContextExceptionMessage) + } } - else { - throw 'Header requires a name when used in an encoding context' - } - } - return $elems + return $elems + } } <# .SYNOPSIS -Creates a new OpenAPI callback component for a given definition tag. + Creates a new OpenAPI callback component for a given definition tag. .DESCRIPTION -The New-PodeOAComponentCallBackInternal function constructs an OpenAPI callback component based on provided parameters. -This function is designed for internal use within the Pode framework to define callbacks in OpenAPI documentation. -It handles the creation of callback structures including the path, HTTP method, request bodies, and responses -based on the given definition tag. + The New-PodeOAComponentCallBackInternal function constructs an OpenAPI callback component based on provided parameters. + This function is designed for internal use within the Pode framework to define callbacks in OpenAPI documentation. + It handles the creation of callback structures including the path, HTTP method, request bodies, and responses + based on the given definition tag. .PARAMETER Params -A hashtable containing parameters for the callback component, such as Method, Path, RequestBody, and Responses. + A hashtable containing parameters for the callback component, such as Method, Path, RequestBody, and Responses. .PARAMETER DefinitionTag -A mandatory string parameter that specifies the definition tag in OpenAPI documentation. + A mandatory string parameter that specifies the definition tag in OpenAPI documentation. .EXAMPLE -$callback = New-PodeOAComponentCallBackInternal -Params $myParams -DefinitionTag 'myTag' + $callback = New-PodeOAComponentCallBackInternal -Params $myParams -DefinitionTag 'myTag' -This example demonstrates how to create an OpenAPI callback component for 'myTag' using the provided parameters. + This example demonstrates how to create an OpenAPI callback component for 'myTag' using the provided parameters. .NOTES -This is an internal function and may change in future releases of Pode. + This is an internal function and may change in future releases of Pode. #> function New-PodeOAComponentCallBackInternal { param( @@ -1767,33 +1952,30 @@ function New-PodeOAComponentCallBackInternal { } - - - <# .SYNOPSIS -Creates a new OpenAPI response object based on provided parameters and a definition tag. + Creates a new OpenAPI response object based on provided parameters and a definition tag. -.DESCRIPTION -The New-PodeOResponseInternal function constructs an OpenAPI response object using provided parameters. -It sets a description for the status code, references existing components if specified, -and builds content-type and header schemas. This function is intended for internal use within the -Pode framework for API documentation purposes. + .DESCRIPTION + The New-PodeOResponseInternal function constructs an OpenAPI response object using provided parameters. + It sets a description for the status code, references existing components if specified, + and builds content-type and header schemas. This function is intended for internal use within the + Pode framework for API documentation purposes. -.PARAMETER Params -A hashtable containing parameters for building the OpenAPI response object, including description, -status code, content, headers, links, and reference to existing components. + .PARAMETER Params + A hashtable containing parameters for building the OpenAPI response object, including description, + status code, content, headers, links, and reference to existing components. -.PARAMETER DefinitionTag -A mandatory string parameter that specifies the definition tag in OpenAPI documentation. + .PARAMETER DefinitionTag + A mandatory string parameter that specifies the definition tag in OpenAPI documentation. -.EXAMPLE -$response = New-PodeOResponseInternal -Params $myParams -DefinitionTag 'myTag' + .EXAMPLE + $response = New-PodeOResponseInternal -Params $myParams -DefinitionTag 'myTag' -This example demonstrates how to create an OpenAPI response object for 'myTag' using the provided parameters. + This example demonstrates how to create an OpenAPI response object for 'myTag' using the provided parameters. -.NOTES -This is an internal function and may change in future releases of Pode. + .NOTES + This is an internal function and may change in future releases of Pode. #> function New-PodeOResponseInternal { param( @@ -1810,11 +1992,12 @@ function New-PodeOResponseInternal { if ($Params.Default) { $Description = 'Default Response.' } - elseif ($Params.StatusCode) { + elseif ([int]::TryParse($Params.StatusCode, [ref]$null)) { $Description = Get-PodeStatusDescription -StatusCode $Params.StatusCode } else { - throw 'A Description is required' + # A Description is required + throw ($PodeLocale.descriptionRequiredExceptionMessage -f $params.Route.path, $Params.StatusCode ) } } else { @@ -1824,7 +2007,7 @@ function New-PodeOResponseInternal { # Handle response referencing an existing component if ($Params.Reference) { Test-PodeOAComponentInternal -Field responses -DefinitionTag $DefinitionTag -Name $Params.Reference -PostValidation - $response = @{ + $response = [ordered]@{ '$ref' = "#/components/responses/$($Params.Reference)" } } @@ -1839,14 +2022,14 @@ function New-PodeOResponseInternal { $_headers = $null if ($null -ne $Params.Headers) { if ($Params.Headers -is [System.Object[]] -or $Params.Headers -is [string] -or $Params.Headers -is [string[]]) { - if ($Params.Headers -is [System.Object[]] -and $Params.Headers.Count -gt 0 -and ($Params.Headers[0] -is [hashtable] -or $Params.Headers[0] -is [ordered])) { + if ($Params.Headers -is [System.Object[]] -and $Params.Headers.Count -gt 0 -and ($Params.Headers[0] -is [hashtable] -or $Params.Headers[0] -is [System.Collections.Specialized.OrderedDictionary])) { $_headers = ConvertTo-PodeOAHeaderProperty -Headers $Params.Headers } else { - $_headers = @{} + $_headers = [ordered]@{} foreach ($h in $Params.Headers) { Test-PodeOAComponentInternal -Field headers -DefinitionTag $DefinitionTag -Name $h -PostValidation - $_headers[$h] = @{ + $_headers[$h] = [ordered]@{ '$ref' = "#/components/headers/$h" } } @@ -1878,24 +2061,24 @@ function New-PodeOResponseInternal { <# .SYNOPSIS -Creates a new OpenAPI response link object. + Creates a new OpenAPI response link object. .DESCRIPTION -The New-PodeOAResponseLinkInternal function generates an OpenAPI response link object from provided parameters. -This includes setting up descriptions, operation IDs, references, parameters, and request bodies for the link. -This function is designed for internal use within the Pode framework to facilitate the creation of response -link objects in OpenAPI documentation. + The New-PodeOAResponseLinkInternal function generates an OpenAPI response link object from provided parameters. + This includes setting up descriptions, operation IDs, references, parameters, and request bodies for the link. + This function is designed for internal use within the Pode framework to facilitate the creation of response + link objects in OpenAPI documentation. .PARAMETER Params -A hashtable of parameters for the OpenAPI response link. + A hashtable of parameters for the OpenAPI response link. .EXAMPLE -$link = New-PodeOAResponseLinkInternal -Params $myParams + $link = New-PodeOAResponseLinkInternal -Params $myParams -Generates a new OpenAPI response link object using the provided parameters in $myParams. + Generates a new OpenAPI response link object using the provided parameters in $myParams. .NOTES -This is an internal function and may change in future releases of Pode. + This is an internal function and may change in future releases of Pode. #> function New-PodeOAResponseLinkInternal { param( @@ -1944,31 +2127,37 @@ function Test-PodeOADefinitionInternal { # Check if the validation result indicates issues if (! $definitionIssues.valid) { # Print a header for undefined OpenAPI references - Write-PodeHost 'Undefined OpenAPI References :' -ForegroundColor Red + # Undefined OpenAPI References + Write-PodeHost $PodeLocale.undefinedOpenApiReferencesMessage -ForegroundColor Red # Iterate over each issue found in the definitions foreach ($tag in $definitionIssues.issues.keys) { - Write-PodeHost "Definition $tag :" -ForegroundColor Red + # Definition tag + Write-PodeHost ($PodeLocale.definitionTagMessage -f $tag) -ForegroundColor Red # Check and display issues related to OpenAPI document generation error if ($definitionIssues.issues[$tag].definition ) { - Write-PodeHost ' OpenAPI generation document error: ' -ForegroundColor Red + # OpenAPI generation document error + Write-PodeHost $PodeLocale.openApiGenerationDocumentErrorMessage -ForegroundColor Red Write-PodeHost " $($definitionIssues.issues[$tag].definition)" -ForegroundColor Red } # Check for missing mandatory 'title' field if ($definitionIssues.issues[$tag].title ) { - Write-PodeHost ' info.title is mandatory' -ForegroundColor Red + # info.title is mandatory + Write-PodeHost $PodeLocale.infoTitleMandatoryMessage -ForegroundColor Red } # Check for missing mandatory 'version' field if ($definitionIssues.issues[$tag].version ) { - Write-PodeHost ' info.version is mandatory' -ForegroundColor Red + # info.version is mandatory + Write-PodeHost $PodeLocale.infoVersionMandatoryMessage -ForegroundColor Red } # Check for missing components and list them if ($definitionIssues.issues[$tag].components ) { - Write-PodeHost ' Missing component(s)' -ForegroundColor Red + # Missing component(s) + Write-PodeHost $PodeLocale.missingComponentsMessage -ForegroundColor Red foreach ($key in $definitionIssues.issues[$tag].components.keys) { $occurences = $definitionIssues.issues[$tag].components[$key] # Adjust occurrence count based on schema validation setting @@ -1984,39 +2173,40 @@ function Test-PodeOADefinitionInternal { } # Throw an error indicating non-compliance with OpenAPI standards - throw 'OpenAPI document compliance issues' + # OpenAPI document compliance issues + throw ($PodeLocale.openApiDocumentNotCompliantExceptionMessage) } } <# .SYNOPSIS -Check the OpenAPI component exist (Internal Function) + Check the OpenAPI component exist (Internal Function) .DESCRIPTION -Check the OpenAPI component exist (Internal Function) + Check the OpenAPI component exist (Internal Function) .PARAMETER Field -The component type + The component type .PARAMETER Name -The component Name + The component Name .PARAMETER DefinitionTag -An Array of strings representing the unique tag for the API specification. -This tag helps in distinguishing between different versions or types of API specifications within the application. -You can use this tag to reference the specific API documentation, schema, or version that your function interacts with. + An Array of strings representing the unique tag for the API specification. + This tag helps in distinguishing between different versions or types of API specifications within the application. + You can use this tag to reference the specific API documentation, schema, or version that your function interacts with. .PARAMETER ThrowException -Generate an exception if the component doesn't exist + Generate an exception if the component doesn't exist .PARAMETER PostValidation -Postpone the check before the server start + Postpone the check before the server start .EXAMPLE -Test-PodeOAComponentInternal -Field 'responses' -Name 'myresponse' -DefinitionTag 'default' + Test-PodeOAComponentInternal -Field 'responses' -Name 'myresponse' -DefinitionTag 'default' .NOTES -This is an internal function and may change in future releases of Pode. + This is an internal function and may change in future releases of Pode. #> function Test-PodeOAComponentInternal { param( @@ -2056,7 +2246,7 @@ function Test-PodeOAComponentInternal { if (!($PodeContext.Server.OpenAPI.Definitions[$tag].components[$field].keys -ccontains $Name)) { # If $Name is not found in the current $tag, return $false or throw an exception if ($ThrowException.IsPresent ) { - throw "No components of type $field named $Name are available in the $tag definition." + throw ($PodeLocale.noComponentInDefinitionExceptionMessage -f $field, $Name, $tag) #"No component of type $field named $Name is available in the $tag definition." } else { return $false diff --git a/src/Private/PodeServer.ps1 b/src/Private/PodeServer.ps1 index 1b7fa9865..776438654 100644 --- a/src/Private/PodeServer.ps1 +++ b/src/Private/PodeServer.ps1 @@ -24,7 +24,7 @@ function Start-PodeWebServer { $endpoints = @() $endpointsMap = @{} - @(Get-PodeEndpoints -Type Http, Ws) | ForEach-Object { + @(Get-PodeEndpointByProtocolType -Type Http, Ws) | ForEach-Object { # get the ip address $_ip = [string]($_.Address) $_ip = Get-PodeIPAddressesForHostname -Hostname $_ip -Type All | Select-Object -First 1 @@ -71,7 +71,7 @@ function Start-PodeWebServer { # create the listener $listener = (. ([scriptblock]::Create("New-Pode$($PodeContext.Server.ListenerType)Listener -CancellationToken `$PodeContext.Tokens.Cancellation.Token"))) $listener.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled) - $listener.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevels) + $listener.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevel) $listener.RequestTimeout = $PodeContext.Server.Request.Timeout $listener.RequestBodySize = $PodeContext.Server.Request.BodySize $listener.ShowServerDetails = [bool]$PodeContext.Server.Security.ServerDetails @@ -103,7 +103,7 @@ function Start-PodeWebServer { } # only if HTTP endpoint - if (Test-PodeEndpoints -Type Http) { + if (Test-PodeEndpointByProtocolType -Type Http) { # script for listening out for incoming requests $listenScript = { param( @@ -169,7 +169,7 @@ function Start-PodeWebServer { # accept/transfer encoding $WebEvent.TransferEncoding = (Get-PodeTransferEncoding -TransferEncoding (Get-PodeHeader -Name 'Transfer-Encoding') -ThrowError) $WebEvent.AcceptEncoding = (Get-PodeAcceptEncoding -AcceptEncoding (Get-PodeHeader -Name 'Accept-Encoding') -ThrowError) - $WebEvent.Ranges = (Get-PodeRanges -Range (Get-PodeHeader -Name 'Range') -ThrowError) + $WebEvent.Ranges = (Get-PodeRange -Range (Get-PodeHeader -Name 'Range') -ThrowError) # add logging endware for post-request Add-PodeRequestLogEndware -WebEvent $WebEvent @@ -234,7 +234,9 @@ function Start-PodeWebServer { } } } - catch [System.OperationCanceledException] {} + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } catch [System.Net.Http.HttpRequestException] { if ($Response.StatusCode -ge 500) { $_.Exception | Write-PodeErrorLog -CheckInnerException @@ -253,7 +255,7 @@ function Start-PodeWebServer { Set-PodeResponseStatus -Code 500 -Exception $_ } finally { - Update-PodeServerRequestMetrics -WebEvent $WebEvent + Update-PodeServerRequestMetric -WebEvent $WebEvent } # invoke endware specifc to the current web event @@ -266,7 +268,9 @@ function Start-PodeWebServer { } } } - catch [System.OperationCanceledException] {} + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } catch { $_ | Write-PodeErrorLog $_.Exception | Write-PodeErrorLog -CheckInnerException @@ -276,12 +280,12 @@ function Start-PodeWebServer { # start the runspace for listening on x-number of threads 1..$PodeContext.Threads.General | ForEach-Object { - Add-PodeRunspace -Type Web -ScriptBlock $listenScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ } + Add-PodeRunspace -Type Web -Name 'Listener' -Id $_ -ScriptBlock $listenScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ } } } # only if WS endpoint - if (Test-PodeEndpoints -Type Ws) { + if (Test-PodeEndpointByProtocolType -Type Ws) { # script to write messages back to the client(s) $signalScript = { param( @@ -322,14 +326,16 @@ function Start-PodeWebServer { # send the message to all found sockets foreach ($socket in $sockets) { try { - $socket.Context.Response.SendSignal($message) + $null = Wait-PodeTask -Task $socket.Context.Response.SendSignal($message) } catch { $null = $Listener.Signals.Remove($socket.ClientId) } } } - catch [System.OperationCanceledException] {} + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } catch { $_ | Write-PodeErrorLog $_.Exception | Write-PodeErrorLog -CheckInnerException @@ -339,7 +345,9 @@ function Start-PodeWebServer { } } } - catch [System.OperationCanceledException] {} + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } catch { $_ | Write-PodeErrorLog $_.Exception | Write-PodeErrorLog -CheckInnerException @@ -347,11 +355,11 @@ function Start-PodeWebServer { } } - Add-PodeRunspace -Type Signals -ScriptBlock $signalScript -Parameters @{ 'Listener' = $listener } + Add-PodeRunspace -Type Signals -Name 'Listener' -ScriptBlock $signalScript -Parameters @{ 'Listener' = $listener } } # only if WS endpoint - if (Test-PodeEndpoints -Type Ws) { + if (Test-PodeEndpointByProtocolType -Type Ws) { # script to queue messages from clients to send back to other clients from the server $clientScript = { param( @@ -405,18 +413,22 @@ function Start-PodeWebServer { Send-PodeSignal -Value $SignalEvent.Data.Message -Path $SignalEvent.Data.Path -ClientId $SignalEvent.Data.ClientId } } - catch [System.OperationCanceledException] {} + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } catch { $_ | Write-PodeErrorLog $_.Exception | Write-PodeErrorLog -CheckInnerException } finally { - Update-PodeServerSignalMetrics -SignalEvent $SignalEvent + Update-PodeServerSignalMetric -SignalEvent $SignalEvent Close-PodeDisposable -Disposable $context } } } - catch [System.OperationCanceledException] {} + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } catch { $_ | Write-PodeErrorLog $_.Exception | Write-PodeErrorLog -CheckInnerException @@ -426,7 +438,7 @@ function Start-PodeWebServer { # start the runspace for listening on x-number of threads 1..$PodeContext.Threads.General | ForEach-Object { - Add-PodeRunspace -Type Signals -ScriptBlock $clientScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ } + Add-PodeRunspace -Type Signals -Name 'Broadcaster' -Id $_ -ScriptBlock $clientScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ } } } @@ -443,7 +455,9 @@ function Start-PodeWebServer { Start-Sleep -Seconds 1 } } - catch [System.OperationCanceledException] {} + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } catch { $_ | Write-PodeErrorLog $_.Exception | Write-PodeErrorLog -CheckInnerException @@ -454,12 +468,13 @@ function Start-PodeWebServer { } } - $waitType = 'Web' - if (!(Test-PodeEndpoints -Type Http)) { - $waitType = 'Signals' - } - Add-PodeRunspace -Type $waitType -ScriptBlock $waitScript -Parameters @{ 'Listener' = $listener } -NoProfile + if (Test-PodeEndpointByProtocolType -Type Http) { + Add-PodeRunspace -Type 'Web' -Name 'KeepAlive' -ScriptBlock $waitScript -Parameters @{ 'Listener' = $listener } -NoProfile + } + else { + Add-PodeRunspace -Type 'Signals' -Name 'KeepAlive' -ScriptBlock $waitScript -Parameters @{ 'Listener' = $listener } -NoProfile + } # browse to the first endpoint, if flagged if ($Browse) { diff --git a/src/Private/Responses.ps1 b/src/Private/Responses.ps1 index 55cc9b516..f16cd7e9c 100644 --- a/src/Private/Responses.ps1 +++ b/src/Private/Responses.ps1 @@ -133,7 +133,7 @@ This is an internal function and may change in future releases of Pode. function Write-PodeFileResponseInternal { [CmdletBinding()] param ( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNull()] [string] $Path, @@ -365,7 +365,7 @@ function Write-PodeDirectoryResponseInternal { RootPath = $RootPath Path = $leaf.Replace('\', '/') WindowsMode = $windowsMode.ToString().ToLower() - FileContent = $htmlContent.ToString() # Convert the StringBuilder content to a string + FileContent = $htmlContent.ToString() # Convert the StringBuilder content to a string } $podeRoot = Get-PodeModuleMiscPath diff --git a/src/Private/Routes.ps1 b/src/Private/Routes.ps1 index 301f45d90..319852e5c 100644 --- a/src/Private/Routes.ps1 +++ b/src/Private/Routes.ps1 @@ -376,10 +376,34 @@ function ConvertTo-PodeOpenApiRoutePath { $Path ) - return (Resolve-PodePlaceholders -Path $Path -Pattern '\:(?[\w]+)' -Prepend '{' -Append '}') + return (Resolve-PodePlaceholder -Path $Path -Pattern '\:(?[\w]+)' -Prepend '{' -Append '}') } -function Update-PodeRouteSlashes { +<# +.SYNOPSIS + Updates a Pode route path to ensure proper formatting. + +.DESCRIPTION + This function takes a Pode route path and ensures that it starts with a leading slash ('/') and follows the correct format for static routes. It also replaces '*' with '.*' for proper regex matching. + +.PARAMETER Path + The Pode route path to update. + +.PARAMETER Static + Indicates whether the route is a static route (default is false). + +.PARAMETER NoLeadingSlash + Indicates whether the route should not have a leading slash (default is false). + +.OUTPUTS + The updated Pode route path. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Update-PodeRouteSlash { + [CmdletBinding()] + [OutputType([string])] param( [Parameter(Mandatory = $true)] [string] @@ -432,8 +456,8 @@ function ConvertTo-PodeRouteRegex { $Path = Protect-PodeValue -Value $Path -Default '/' $Path = Split-PodeRouteQuery -Path $Path $Path = Protect-PodeValue -Value $Path -Default '/' - $Path = Update-PodeRouteSlashes -Path $Path - $Path = Resolve-PodePlaceholders -Path $Path + $Path = Update-PodeRouteSlash -Path $Path + $Path = Resolve-PodePlaceholder -Path $Path return $Path } @@ -504,10 +528,10 @@ function Test-PodeRouteInternal { } if ([string]::IsNullOrEmpty($_url)) { - throw "[$($Method)] $($Path): Already defined" + throw ($PodeLocale.methodPathAlreadyDefinedExceptionMessage -f $Method, $Path) #"[$($Method)] $($Path): Already defined" } - throw "[$($Method)] $($Path): Already defined for $($_url)" + throw ($PodeLocale.methodPathAlreadyDefinedForUrlExceptionMessage -f $Method, $Path, $_url) #"[$($Method)] $($Path): Already defined for $($_url)" } function Convert-PodeFunctionVerbToHttpMethod { @@ -652,17 +676,19 @@ function ConvertTo-PodeMiddleware { # check middleware is a type valid if (($mid -isnot [scriptblock]) -and ($mid -isnot [hashtable])) { - throw "One of the Middlewares supplied is an invalid type. Expected either a ScriptBlock or Hashtable, but got: $($mid.GetType().Name)" + throw ($PodeLocale.invalidMiddlewareTypeExceptionMessage -f $mid.GetType().Name)#"One of the Middlewares supplied is an invalid type. Expected either a ScriptBlock or Hashtable, but got: $($mid.GetType().Name)" } # if middleware is hashtable, ensure the keys are valid (logic is a scriptblock) if ($mid -is [hashtable]) { if ($null -eq $mid.Logic) { - throw 'A Hashtable Middleware supplied has no Logic defined' + # A Hashtable Middleware supplied has no Logic defined + throw ($PodeLocale.hashtableMiddlewareNoLogicExceptionMessage) } if ($mid.Logic -isnot [scriptblock]) { - throw "A Hashtable Middleware supplied has an invalid Logic type. Expected ScriptBlock, but got: $($mid.Logic.GetType().Name)" + # A Hashtable Middleware supplied has an invalid Logic type. Expected ScriptBlock, but got: {0} + throw ($PodeLocale.invalidLogicTypeInHashtableMiddlewareExceptionMessage -f $mid.Logic.GetType().Name) } } } diff --git a/src/Private/Runspaces.ps1 b/src/Private/Runspaces.ps1 new file mode 100644 index 000000000..7bedb2f33 --- /dev/null +++ b/src/Private/Runspaces.ps1 @@ -0,0 +1,321 @@ +<# +.SYNOPSIS + Adds a new runspace to Pode with the specified type and script block. + +.DESCRIPTION + The `Add-PodeRunspace` function creates a new PowerShell runspace within Pode + based on the provided type and script block. This function allows for additional + customization through parameters, output streaming, and runspace management options. + +.PARAMETER Type + The type of runspace to create. Accepted values are: + 'Main', 'Signals', 'Schedules', 'Gui', 'Web', 'Smtp', 'Tcp', 'Tasks', + 'WebSockets', 'Files', 'Timers'. + +.PARAMETER ScriptBlock + The script block to execute within the runspace. This script block will be + added to the runspace's pipeline. + +.PARAMETER Parameters + Optional parameters to pass to the script block. + +.PARAMETER OutputStream + A PSDataCollection object to handle output streaming for the runspace. + +.PARAMETER Forget + If specified, the pipeline's output will not be stored or remembered. + +.PARAMETER NoProfile + If specified, the runspace will not load any modules or profiles. + +.PARAMETER PassThru + If specified, returns the pipeline and handler for custom processing. + +.EXAMPLE + Add-PodeRunspace -Type 'Tasks' -ScriptBlock { + # Your script code here + } +#> + +function Add-PodeRunspace { + param( + [Parameter(Mandatory = $true)] + [ValidateSet('Main', 'Signals', 'Schedules', 'Gui', 'Web', 'Smtp', 'Tcp', 'Tasks', 'WebSockets', 'Files', 'Timers')] + [string] + $Type, + + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [scriptblock] + $ScriptBlock, + + [Parameter()] + $Parameters, + + [Parameter()] + [System.Management.Automation.PSDataCollection[psobject]] + $OutputStream = $null, + + [switch] + $Forget, + + [switch] + $NoProfile, + + [switch] + $PassThru, + + [string] + $Name, + + [string] + $Id = '1' + ) + + try { + # Define the script block to open the runspace and set its state. + $openRunspaceScript = { + param($Type, $Name, $NoProfile) + try { + # Set the runspace name. + Set-PodeCurrentRunspaceName -Name $Name + + if (!$NoProfile) { + # Import necessary internal Pode modules for the runspace. + Import-PodeModulesInternal + + # Add required PowerShell drives. + Add-PodePSDrivesInternal + } + + # Mark the runspace as 'Ready' to process requests. + $PodeContext.RunspacePools[$Type].State = 'Ready' + } + catch { + # Handle errors, setting the runspace state to 'Error' if applicable. + if ($PodeContext.RunspacePools[$Type].State -ieq 'waiting') { + $PodeContext.RunspacePools[$Type].State = 'Error' + } + + # Output the error details to the default stream and rethrow. + $_ | Out-Default + $_.ScriptStackTrace | Out-Default + throw + } + } + + # Create a PowerShell pipeline. + $ps = [powershell]::Create() + $ps.RunspacePool = $PodeContext.RunspacePools[$Type].Pool + + # Add the script block and parameters to the pipeline. + $null = $ps.AddScript($openRunspaceScript) + $null = $ps.AddParameters( + @{ + 'Type' = $Type + 'Name' = "Pode_$($Type)_$($Name)_$($Id)" + 'NoProfile' = $NoProfile.IsPresent + } + ) + + # Add the main script block to the pipeline. + $null = $ps.AddScript($ScriptBlock) + + # Add any provided parameters to the script block. + if (!(Test-PodeIsEmpty $Parameters)) { + $Parameters.Keys | ForEach-Object { + $null = $ps.AddParameter($_, $Parameters[$_]) + } + } + + # Begin invoking the pipeline, with or without output streaming. + if ($null -eq $OutputStream) { + $pipeline = $ps.BeginInvoke() + } + else { + $pipeline = $ps.BeginInvoke($OutputStream, $OutputStream) + } + + # Handle forgetting, returning, or storing the pipeline. + if ($Forget) { + $null = $pipeline + } + elseif ($PassThru) { + return @{ + Pipeline = $ps + Handler = $pipeline + } + } + else { + $PodeContext.Runspaces += @{ + Pool = $Type + Pipeline = $ps + Handler = $pipeline + Stopped = $false + } + } + } + catch { + # Log and throw any exceptions encountered during execution. + $_ | Write-PodeErrorLog + throw $_.Exception + } +} + +<# +.SYNOPSIS + Closes and disposes of the Pode runspaces, listeners, receivers, watchers, and optionally runspace pools. + +.DESCRIPTION + This function checks and waits for all Listeners, Receivers, and Watchers to be disposed of + before proceeding to close and dispose of the runspaces and optionally the runspace pools. + It ensures a clean shutdown by managing the disposal of resources in a specified order. + The function handles serverless and regular server environments differently, skipping + disposal actions in serverless contexts. + +.PARAMETER ClosePool + Specifies whether to close and dispose of the runspace pools along with the runspaces. + This is optional and should be specified if the pools need to be explicitly closed. + +.EXAMPLE + Close-PodeRunspace -ClosePool + This example closes all runspaces and their associated pools, ensuring that all resources are properly disposed of. + +.OUTPUTS + None + Outputs from this function are primarily internal state changes and verbose logging. +#> +function Close-PodeRunspace { + param( + [switch] + $ClosePool + ) + + # Early return if server is serverless, as disposal is not required. + if ($PodeContext.Server.IsServerless) { + return + } + + try { + # Only proceed if there are runspaces to dispose of. + if (!(Test-PodeIsEmpty $PodeContext.Runspaces)) { + Write-Verbose 'Waiting until all Listeners are disposed' + + $count = 0 + $continue = $false + # Attempts to dispose of resources for up to 10 seconds. + while ($count -le 10) { + Start-Sleep -Seconds 1 + $count++ + + $continue = $false + # Check each listener, receiver, and watcher; if any are not disposed, continue waiting. + foreach ($listener in $PodeContext.Listeners) { + if (!$listener.IsDisposed) { + $continue = $true + break + } + } + + foreach ($receiver in $PodeContext.Receivers) { + if (!$receiver.IsDisposed) { + $continue = $true + break + } + } + + foreach ($watcher in $PodeContext.Watchers) { + if (!$watcher.IsDisposed) { + $continue = $true + break + } + } + # If undisposed resources exist, continue waiting. + if ($continue) { + continue + } + + break + } + + Write-Verbose 'All Listeners disposed' + + # now dispose runspaces + Write-Verbose 'Disposing Runspaces' + $runspaceErrors = @(foreach ($item in $PodeContext.Runspaces) { + if ($item.Stopped) { + continue + } + + try { + # only do this, if the pool is in error + if ($PodeContext.RunspacePools[$item.Pool].State -ieq 'error') { + $item.Pipeline.EndInvoke($item.Handler) + } + } + catch { + "$($item.Pool) runspace failed to load: $($_.Exception.InnerException.Message)" + } + + Close-PodeDisposable -Disposable $item.Pipeline + $item.Stopped = $true + }) + + # dispose of schedule runspaces + if ($PodeContext.Schedules.Processes.Count -gt 0) { + foreach ($key in $PodeContext.Schedules.Processes.Keys.Clone()) { + Close-PodeScheduleInternal -Process $PodeContext.Schedules.Processes[$key] + } + } + + # dispose of task runspaces + if ($PodeContext.Tasks.Processes.Count -gt 0) { + foreach ($key in $PodeContext.Tasks.Processes.Keys.Clone()) { + Close-PodeTaskInternal -Process $PodeContext.Tasks.Processes[$key] + } + } + + $PodeContext.Runspaces = @() + Write-Verbose 'Runspaces disposed' + } + + # close/dispose the runspace pools + if ($ClosePool) { + Close-PodeRunspacePool + } + + # Check for and throw runspace errors if any occurred during disposal. + if (($null -ne $runspaceErrors) -and ($runspaceErrors.Length -gt 0)) { + foreach ($err in $runspaceErrors) { + if ($null -eq $err) { + continue + } + + throw $err + } + } + + # garbage collect + Invoke-PodeGC + } + catch { + $_ | Write-PodeErrorLog + throw $_.Exception + } +} + + + + + + + + + + + + + + + + diff --git a/src/Private/Schedules.ps1 b/src/Private/Schedules.ps1 index a59981ef8..924a888d2 100644 --- a/src/Private/Schedules.ps1 +++ b/src/Private/Schedules.ps1 @@ -12,72 +12,101 @@ function Find-PodeSchedule { function Test-PodeSchedulesExist { return (($null -ne $PodeContext.Schedules) -and (($PodeContext.Schedules.Enabled) -or ($PodeContext.Schedules.Items.Count -gt 0))) } - function Start-PodeScheduleRunspace { + if (!(Test-PodeSchedulesExist)) { return } - Add-PodeSchedule -Name '__pode_schedule_housekeeper__' -Cron '@minutely' -ScriptBlock { - if ($PodeContext.Schedules.Processes.Count -eq 0) { - return - } + Add-PodeTimer -Name '__pode_schedule_housekeeper__' -Interval 30 -ScriptBlock { + try { + if ($PodeContext.Schedules.Processes.Count -eq 0) { + return + } + + $now = [datetime]::UtcNow - foreach ($key in $PodeContext.Schedules.Processes.Keys.Clone()) { - $process = $PodeContext.Schedules.Processes[$key] + foreach ($key in $PodeContext.Schedules.Processes.Keys.Clone()) { + try { + $process = $PodeContext.Schedules.Processes[$key] - # is it completed? - if (!$process.Runspace.Handler.IsCompleted) { - continue + # if it's completed or expired, dispose and remove + if ($process.Runspace.Handler.IsCompleted -or ($process.ExpireTime -lt $now)) { + Close-PodeScheduleInternal -Process $process + } + } + catch { + $_ | Write-PodeErrorLog + } } - # dispose and remove the schedule process - Close-PodeScheduleInternal -Process $process + $process = $null + } + catch { + $_ | Write-PodeErrorLog } - - $process = $null } $script = { - # select the schedules that trigger on-start - $_now = [DateTime]::Now - - $PodeContext.Schedules.Items.Values | - Where-Object { - $_.OnStart - } | ForEach-Object { - Invoke-PodeInternalSchedule -Schedule $_ - } - - # complete any schedules - Complete-PodeInternalSchedules -Now $_now + try { - # first, sleep for a period of time to get to 00 seconds (start of minute) - Start-Sleep -Seconds (60 - [DateTime]::Now.Second) - - while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + # select the schedules that trigger on-start $_now = [DateTime]::Now - # select the schedules that need triggering $PodeContext.Schedules.Items.Values | Where-Object { - !$_.Completed -and - (($null -eq $_.StartTime) -or ($_.StartTime -le $_now)) -and - (($null -eq $_.EndTime) -or ($_.EndTime -ge $_now)) -and - (Test-PodeCronExpressions -Expressions $_.Crons -DateTime $_now) + $_.OnStart } | ForEach-Object { Invoke-PodeInternalSchedule -Schedule $_ } # complete any schedules - Complete-PodeInternalSchedules -Now $_now + Complete-PodeInternalSchedule -Now $_now - # cron expression only goes down to the minute, so sleep for 1min + # first, sleep for a period of time to get to 00 seconds (start of minute) Start-Sleep -Seconds (60 - [DateTime]::Now.Second) + + while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + try { + $_now = [DateTime]::Now + + # select the schedules that need triggering + $PodeContext.Schedules.Items.Values | + Where-Object { + !$_.Completed -and + (($null -eq $_.StartTime) -or ($_.StartTime -le $_now)) -and + (($null -eq $_.EndTime) -or ($_.EndTime -ge $_now)) -and + (Test-PodeCronExpressions -Expressions $_.Crons -DateTime $_now) + } | ForEach-Object { + try { + Invoke-PodeInternalSchedule -Schedule $_ + } + catch { + $_ | Write-PodeErrorLog + } + } + + # complete any schedules + Complete-PodeInternalSchedule -Now $_now + + # cron expression only goes down to the minute, so sleep for 1min + Start-Sleep -Seconds (60 - [DateTime]::Now.Second) + } + catch { + $_ | Write-PodeErrorLog + } + } + } + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } + catch { + $_ | Write-PodeErrorLog + throw $_.Exception } } - Add-PodeRunspace -Type Main -ScriptBlock $script -NoProfile + Add-PodeRunspace -Type Main -Name 'Schedules' -ScriptBlock $script -NoProfile } function Close-PodeScheduleInternal { @@ -95,24 +124,44 @@ function Close-PodeScheduleInternal { $null = $PodeContext.Schedules.Processes.Remove($Process.ID) } -function Complete-PodeInternalSchedules { +<# +.SYNOPSIS + Completes schedules that have exceeded their end time. + +.DESCRIPTION + The `Complete-PodeInternalSchedule` function checks for schedules that have an end time + and marks them as completed if their end time is earlier than the current time. + +.PARAMETER Now + Specifies the current date and time. This parameter is mandatory. + +.INPUTS + None. You cannot pipe objects to Complete-PodeInternalSchedule. + +.OUTPUTS + None. The function modifies the state of schedules in the PodeContext. + +.EXAMPLE + # Example usage: + $now = Get-Date + Complete-PodeInternalSchedule -Now $now + # Schedules that have ended are marked as completed. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Complete-PodeInternalSchedule { param( [Parameter(Mandatory = $true)] [datetime] $Now ) - # add any schedules to remove that have exceeded their end time - $Schedules = @($PodeContext.Schedules.Items.Values | - Where-Object { (($null -ne $_.EndTime) -and ($_.EndTime -lt $Now)) }) - - if (($null -eq $Schedules) -or ($Schedules.Length -eq 0)) { - return - } - # set any expired schedules as being completed - $Schedules | ForEach-Object { - $_.Completed = $true + foreach ($schedule in $PodeContext.Schedules.Items.Values) { + if (($null -ne $schedule.EndTime) -and ($schedule.EndTime -lt $Now)) { + $schedule.Completed = $true + } } } @@ -156,6 +205,7 @@ function Invoke-PodeInternalSchedule { function Invoke-PodeInternalScheduleLogic { param( [Parameter(Mandatory = $true)] + [hashtable] $Schedule, [Parameter()] @@ -164,44 +214,118 @@ function Invoke-PodeInternalScheduleLogic { ) try { + # generate processId for schedule + $processId = New-PodeGuid + # setup event param $parameters = @{ - Event = @{ - Lockable = $PodeContext.Threading.Lockables.Global - Sender = $Schedule - Metadata = @{} - } + ProcessId = $processId + ArgumentList = $ArgumentList } - # add any schedule args - foreach ($key in $Schedule.Arguments.Keys) { - $parameters[$key] = $Schedule.Arguments[$key] + # what is the expire time if using "create" timeout? + $expireTime = [datetime]::MaxValue + $createTime = [datetime]::UtcNow + + if (($Schedule.Timeout.From -ieq 'Create') -and ($Schedule.Timeout.Value -ge 0)) { + $expireTime = $createTime.AddSeconds($Schedule.Timeout.Value) } + # add the schedule process + $PodeContext.Schedules.Processes[$processId] = @{ + ID = $processId + Schedule = $Schedule.Name + Runspace = $null + CreateTime = $createTime + StartTime = $null + ExpireTime = $expireTime + Timeout = $Schedule.Timeout + State = 'Pending' + } + + # start the schedule runspace + $scriptblock = Get-PodeScheduleScriptBlock + $runspace = Add-PodeRunspace -Type Schedules -Name $Schedule.Name -ScriptBlock $scriptblock -Parameters $parameters -PassThru + # add runspace to process + $PodeContext.Schedules.Processes[$processId].Runspace = $runspace + } + catch { + $_ | Write-PodeErrorLog + } +} + +function Get-PodeScheduleScriptBlock { + return { + param($ProcessId, $ArgumentList) - # add adhoc schedule invoke args - if (($null -ne $ArgumentList) -and ($ArgumentList.Count -gt 0)) { - foreach ($key in $ArgumentList.Keys) { - $parameters[$key] = $ArgumentList[$key] + try { + # get the schedule process, error if not found + $process = $PodeContext.Schedules.Processes[$ProcessId] + if ($null -eq $process) { + # Schedule process does not exist: $ProcessId + throw ($PodeLocale.scheduleProcessDoesNotExistExceptionMessage -f $ProcessId) } - } - # add any using variables - if ($null -ne $Schedule.UsingVariables) { - foreach ($usingVar in $Schedule.UsingVariables) { - $parameters[$usingVar.NewName] = $usingVar.Value + # set start time and state + $process.StartTime = [datetime]::UtcNow + $process.State = 'Running' + + # set expire time if timeout based on "start" time + if (($process.Timeout.From -ieq 'Start') -and ($process.Timeout.Value -ge 0)) { + $process.ExpireTime = $process.StartTime.AddSeconds($process.Timeout.Value) } - } - $name = New-PodeGuid - $runspace = Add-PodeRunspace -Type Schedules -ScriptBlock (($Schedule.Script).GetNewClosure()) -Parameters $parameters -PassThru + # get the schedule, error if not found + $schedule = Find-PodeSchedule -Name $process.Schedule + if ($null -eq $schedule) { + throw ($PodeLocale.scheduleDoesNotExistExceptionMessage -f $process.Schedule) + } + + # build the script arguments + $ScheduleEvent = @{ + Lockable = $PodeContext.Threading.Lockables.Global + Sender = $schedule + Timestamp = [DateTime]::UtcNow + Metadata = @{} + } + + $_args = @{ Event = $ScheduleEvent } + + if ($null -ne $schedule.Arguments) { + foreach ($key in $schedule.Arguments.Keys) { + $_args[$key] = $schedule.Arguments[$key] + } + } + + if ($null -ne $ArgumentList) { + foreach ($key in $ArgumentList.Keys) { + $_args[$key] = $ArgumentList[$key] + } + } + + # add any using variables + if ($null -ne $schedule.UsingVariables) { + foreach ($usingVar in $schedule.UsingVariables) { + $_args[$usingVar.NewName] = $usingVar.Value + } + } - $PodeContext.Schedules.Processes[$name] = @{ - ID = $name - Schedule = $Schedule.Name - Runspace = $runspace + # invoke the script from the schedule + Invoke-PodeScriptBlock -ScriptBlock $schedule.Script -Arguments $_args -Scoped -Splat + + # set state to completed + $process.State = 'Completed' + } + catch { + # update the state + if ($null -ne $process) { + $process.State = 'Failed' + } + + # log the error + $_ | Write-PodeErrorLog + } + finally { + Invoke-PodeGC } - } - catch { - $_ | Write-PodeErrorLog } } \ No newline at end of file diff --git a/src/Private/ScopedVariables.ps1 b/src/Private/ScopedVariables.ps1 index 5fe266963..2991e13e3 100644 --- a/src/Private/ScopedVariables.ps1 +++ b/src/Private/ScopedVariables.ps1 @@ -27,7 +27,7 @@ function Add-PodeScopedVariableInternal { # check if var already defined if (Test-PodeScopedVariable -Name $Name) { - throw "Scoped Variable already defined: $($Name)" + throw ($PodeLocale.scopedVariableAlreadyDefinedExceptionMessage -f $Name)#"Scoped Variable already defined: $($Name)" } # add scoped var definition @@ -69,7 +69,7 @@ function Add-PodeScopedVariableInbuiltSecret { function Add-PodeScopedVariableInbuiltSession { Add-PodeScopedVariable -Name 'session' ` - -SetReplace "`$WebEvent.Session.Data.'{{name}}'" ` + -SetReplace "`$WebEvent.Session.Data.'{{name}}' = " ` -GetReplace "`$WebEvent.Session.Data.'{{name}}'" } @@ -119,35 +119,98 @@ function Convert-PodeScopedVariableInbuiltUsing { } # get any using variables - $usingVars = Get-PodeScopedVariableUsingVariables -ScriptBlock $ScriptBlock + $usingVars = Get-PodeScopedVariableUsingVariable -ScriptBlock $ScriptBlock if (($null -eq $usingVars) -or ($usingVars.Count -eq 0)) { return $ScriptBlock, $null } # convert any using vars to use new names - $usingVars = Find-PodeScopedVariableUsingVariableValues -UsingVariables $usingVars -PSSession $PSSession + $usingVars = Find-PodeScopedVariableUsingVariableValue -UsingVariable $usingVars -PSSession $PSSession # now convert the script - $newScriptBlock = Convert-PodeScopedVariableUsingVariables -ScriptBlock $ScriptBlock -UsingVariables $usingVars + $newScriptBlock = Convert-PodeScopedVariableUsingVariable -ScriptBlock $ScriptBlock -UsingVariables $usingVars # return converted script return $newScriptBlock, $usingVars } -function Get-PodeScopedVariableUsingVariables { +<# +.SYNOPSIS + Retrieves all occurrences of using variables within a given script block. + +.DESCRIPTION + The `Get-PodeScopedVariableUsingVariable` function analyzes a script block and identifies all instances of using variables. + It returns an array of `UsingExpressionAst` objects representing these occurrences. + +.PARAMETER ScriptBlock + Specifies the script block to analyze. This parameter is mandatory. + +.OUTPUTS + Returns an array of `UsingExpressionAst` objects representing using variables found in the script block. + +.EXAMPLE + # Example usage: + $scriptBlock = { + $usingVar1 = "Hello" + $usingVar2 = "World" + Write-Host "Using variables: $usingVar1, $usingVar2" + } + + $usingVariables = Get-PodeScopedVariableUsingVariable -ScriptBlock $scriptBlock + # Process the identified using variables as needed. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Get-PodeScopedVariableUsingVariable { param( [Parameter(Mandatory = $true)] [scriptblock] $ScriptBlock ) + # Analyze the script block AST to find using variables return $ScriptBlock.Ast.FindAll({ $args[0] -is [System.Management.Automation.Language.UsingExpressionAst] }, $true) } -function Find-PodeScopedVariableUsingVariableValues { +<# +.SYNOPSIS + Finds and maps using variables within a given script block to their corresponding values. + +.DESCRIPTION + The `Find-PodeScopedVariableUsingVariableValue` function analyzes a collection of using variables + (represented as `UsingExpressionAst` objects) within a script block. It retrieves the values of these + variables from the specified session state (`$PSSession`) and maps them for further processing. + +.PARAMETER UsingVariable + Specifies an array of `UsingExpressionAst` objects representing using variables found in the script block. + This parameter is mandatory. + +.PARAMETER PSSession + Specifies the session state from which to retrieve variable values. This parameter is mandatory. + +.OUTPUTS + Returns an array of custom objects, each containing the following properties: + - `OldName`: The original expression text for the using variable. + - `NewName`: The modified name for the using variable (prefixed with "__using_"). + - `NewNameWithDollar`: The modified name with a dollar sign prefix (e.g., `$__using_VariableName`). + - `SubExpressions`: An array of sub-expressions associated with the using variable. + - `Value`: The value of the using variable retrieved from the session state. + +.EXAMPLE + # Example usage: + $usingVariables = Get-PodeScopedVariableUsingVariable -ScriptBlock $scriptBlock + $mappedVariables = Find-PodeScopedVariableUsingVariableValue -UsingVariable $usingVariables -PSSession $sessionState + # Process the mapped variables as needed. + +.NOTES + - The function handles both direct using variables and child script using variables (prefixed with "__using_"). + - This is an internal function and may change in future releases of Pode. +#> +function Find-PodeScopedVariableUsingVariableValue { param( [Parameter(Mandatory = $true)] - $UsingVariables, + $UsingVariable, [Parameter(Mandatory = $true)] [System.Management.Automation.SessionState] @@ -156,8 +219,8 @@ function Find-PodeScopedVariableUsingVariableValues { $mapped = @{} - foreach ($usingVar in $UsingVariables) { - # var name + foreach ($usingVar in $UsingVariable) { + # Extract variable name $varName = $usingVar.SubExpression.VariablePath.UserPath # only retrieve value if new var @@ -169,10 +232,10 @@ function Find-PodeScopedVariableUsingVariableValues { } if ([string]::IsNullOrEmpty($value)) { - throw "Value for `$using:$($varName) could not be found" + throw ($PodeLocale.valueForUsingVariableNotFoundExceptionMessage -f $varName) #"Value for `$using:$($varName) could not be found" } - # add to mapped + # Add to mapped variables $mapped[$varName] = @{ OldName = $usingVar.SubExpression.Extent.Text NewName = "__using_$($varName)" @@ -182,14 +245,56 @@ function Find-PodeScopedVariableUsingVariableValues { } } - # add the vars sub-expression for replacing later + # Add the variable's sub-expression for later replacement $mapped[$varName].SubExpressions += $usingVar.SubExpression } return @($mapped.Values) } -function Convert-PodeScopedVariableUsingVariables { +<# +.SYNOPSIS + Converts a script block by replacing using variables with their corresponding values. + +.DESCRIPTION + The `Convert-PodeScopedVariableUsingVariable` function takes a script block and a collection of using variables. + It replaces the using variables within the script block with their associated values. + +.PARAMETER ScriptBlock + Specifies the script block to convert. This parameter is mandatory. + +.PARAMETER UsingVariables + Specifies an array of custom objects representing using variables and their values. + Each object should have the following properties: + - `OldName`: The original expression text for the using variable. + - `NewNameWithDollar`: The modified name with a dollar sign prefix (e.g., `$__using_VariableName`). + - `SubExpressions`: An array of sub-expressions associated with the using variable. + - `Value`: The value of the using variable. + +.OUTPUTS + Returns a new script block with replaced using variables. + +.EXAMPLE + # Example usage: + $usingVariables = @( + @{ + OldName = '$usingVar1' + NewNameWithDollar = '$__using_usingVar1' + SubExpressions = @($usingVar1.SubExpression1, $usingVar1.SubExpression2) + Value = 'SomeValue1' + }, + # Add other using variables here... + ) + + $convertedScriptBlock = Convert-PodeScopedVariableUsingVariable -ScriptBlock $originalScriptBlock -UsingVariables $usingVariables + # Use the converted script block as needed. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Convert-PodeScopedVariableUsingVariable { + [CmdletBinding()] + [OutputType([scriptblock])] param( [Parameter(Mandatory = $true)] [scriptblock] @@ -199,9 +304,9 @@ function Convert-PodeScopedVariableUsingVariables { [hashtable[]] $UsingVariables ) - - $varsList = New-Object 'System.Collections.Generic.List`1[System.Management.Automation.Language.VariableExpressionAst]' - $newParams = New-Object System.Collections.ArrayList + # Create a list of variable expressions for replacement + $varsList = [System.Collections.Generic.List[System.Management.Automation.Language.VariableExpressionAst]]::new() + $newParams = [System.Collections.ArrayList]::new() foreach ($usingVar in $UsingVariables) { foreach ($subExp in $usingVar.SubExpressions) { @@ -209,10 +314,12 @@ function Convert-PodeScopedVariableUsingVariables { } } + # Create a comma-separated list of new parameters $null = $newParams.AddRange(@($UsingVariables.NewNameWithDollar)) $newParams = ($newParams -join ', ') $tupleParams = [tuple]::Create($varsList, $newParams) + # Invoke the internal method to replace variables in the script block $bindingFlags = [System.Reflection.BindingFlags]'Default, NonPublic, Instance' $_varReplacerMethod = $ScriptBlock.Ast.GetType().GetMethod('GetWithInputHandlingForInvokeCommandImpl', $bindingFlags) $convertedScriptBlockStr = $_varReplacerMethod.Invoke($ScriptBlock.Ast, @($tupleParams)) @@ -223,6 +330,7 @@ function Convert-PodeScopedVariableUsingVariables { $convertedScriptBlock = [scriptblock]::Create($convertedScriptBlockStr) + # Handle cases where the script block starts with '$input |' if ($convertedScriptBlock.Ast.EndBlock[0].Statements.Extent.Text.StartsWith('$input |')) { $convertedScriptBlockStr = ($convertedScriptBlockStr -ireplace '\$input \|') $convertedScriptBlock = [scriptblock]::Create($convertedScriptBlockStr) diff --git a/src/Private/Secrets.ps1 b/src/Private/Secrets.ps1 index 744ddd31c..b3248d6c7 100644 --- a/src/Private/Secrets.ps1 +++ b/src/Private/Secrets.ps1 @@ -1,6 +1,6 @@ function Initialize-PodeSecretVault { param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [hashtable] $VaultConfig, @@ -8,13 +8,14 @@ function Initialize-PodeSecretVault { [scriptblock] $ScriptBlock ) - - $null = Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -Splat -Arguments @($VaultConfig.Parameters) + process { + $null = Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -Splat -Arguments @($VaultConfig.Parameters) + } } function Register-PodeSecretManagementVault { param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [hashtable] $VaultConfig, @@ -26,99 +27,112 @@ function Register-PodeSecretManagementVault { [string] $ModuleName ) + begin { + $pipelineItemCount = 0 + } - # use the Name for VaultName if not passed - if ([string]::IsNullOrWhiteSpace($VaultName)) { - $VaultName = $VaultConfig.Name + process { + $pipelineItemCount++ } - # import the modules - $null = Import-Module -Name Microsoft.PowerShell.SecretManagement -Force -DisableNameChecking -Scope Global -ErrorAction Stop -Verbose:$false - $null = Import-Module -Name $ModuleName -Force -DisableNameChecking -Scope Global -ErrorAction Stop -Verbose:$false + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + # use the Name for VaultName if not passed + if ([string]::IsNullOrWhiteSpace($VaultName)) { + $VaultName = $VaultConfig.Name + } - # export the modules for pode - Export-PodeModule -Name @('Microsoft.PowerShell.SecretManagement', $ModuleName) + # import the modules + $null = Import-Module -Name Microsoft.PowerShell.SecretManagement -Force -DisableNameChecking -Scope Global -ErrorAction Stop -Verbose:$false + $null = Import-Module -Name $ModuleName -Force -DisableNameChecking -Scope Global -ErrorAction Stop -Verbose:$false - # is this the local SecretStore provider? - $isSecretStore = ($ModuleName -ieq 'Microsoft.PowerShell.SecretStore') + # export the modules for pode + Export-PodeModule -Name @('Microsoft.PowerShell.SecretManagement', $ModuleName) - # check if we have an unlock password for local secret store - if ($isSecretStore) { - if ([string]::IsNullOrEmpty($VaultConfig.Unlock.Secret)) { - throw 'An "-UnlockSecret" is required when using Microsoft.PowerShell.SecretStore' - } - } + # is this the local SecretStore provider? + $isSecretStore = ($ModuleName -ieq 'Microsoft.PowerShell.SecretStore') - # does the local secret store already exist? - $secretStoreExists = ($isSecretStore -and (Test-PodeSecretVaultInternal -Name $VaultName)) + # check if we have an unlock password for local secret store + if ($isSecretStore) { + if ([string]::IsNullOrEmpty($VaultConfig.Unlock.Secret)) { + # An 'UnlockSecret' is required when using Microsoft.PowerShell.SecretStore + throw ($PodeLocale.unlockSecretRequiredExceptionMessage) + } + } - # do we have vault params? - $hasVaultParams = ($null -ne $VaultConfig.Parameters) + # does the local secret store already exist? + $secretStoreExists = ($isSecretStore -and (Test-PodeSecretVaultInternal -Name $VaultName)) - # attempt to register the vault - $registerParams = @{ - Name = $VaultName - ModuleName = $ModuleName - Confirm = $false - AllowClobber = $true - ErrorAction = 'Stop' - } + # do we have vault params? + $hasVaultParams = ($null -ne $VaultConfig.Parameters) - if (!$isSecretStore -and $hasVaultParams) { - $registerParams['VaultParameters'] = $VaultConfig.Parameters - } + # attempt to register the vault + $registerParams = @{ + Name = $VaultName + ModuleName = $ModuleName + Confirm = $false + AllowClobber = $true + ErrorAction = 'Stop' + } - $null = Register-SecretVault @registerParams + if (!$isSecretStore -and $hasVaultParams) { + $registerParams['VaultParameters'] = $VaultConfig.Parameters + } - # all is good, so set the config - $VaultConfig['SecretManagement'] = @{ - VaultName = $VaultName - ModuleName = $ModuleName - } + $null = Register-SecretVault @registerParams - # set local secret store config - if ($isSecretStore) { - if (!$hasVaultParams) { - $VaultConfig.Parameters = @{} + # all is good, so set the config + $VaultConfig['SecretManagement'] = @{ + VaultName = $VaultName + ModuleName = $ModuleName } - $vaultParams = $VaultConfig.Parameters + # set local secret store config + if ($isSecretStore) { + if (!$hasVaultParams) { + $VaultConfig.Parameters = @{} + } - # remove the password - $vaultParams.Remove('Password') + $vaultParams = $VaultConfig.Parameters - # set default authentication and interaction flags - if ([string]::IsNullOrEmpty($vaultParams.Authentication)) { - $vaultParams['Authentication'] = 'Password' - } + # remove the password + $vaultParams.Remove('Password') - if ([string]::IsNullOrEmpty($vaultParams.Interaction)) { - $vaultParams['Interaction'] = 'None' - } + # set default authentication and interaction flags + if ([string]::IsNullOrEmpty($vaultParams.Authentication)) { + $vaultParams['Authentication'] = 'Password' + } - # set default password timeout and unlock interval to 1 minute - if ($VaultConfig.Unlock.Interval -le 0) { - $VaultConfig.Unlock.Interval = 1 - } + if ([string]::IsNullOrEmpty($vaultParams.Interaction)) { + $vaultParams['Interaction'] = 'None' + } - # unlock the vault, and set password - $VaultConfig | Unlock-PodeSecretManagementVault + # set default password timeout and unlock interval to 1 minute + if ($VaultConfig.Unlock.Interval -le 0) { + $VaultConfig.Unlock.Interval = 1 + } - # set the password timeout for the vault - if (!$secretStoreExists) { - if ($VaultConfig.Parameters.PasswordTimeout -le 0) { - $vaultParams['PasswordTimeout'] = ($VaultConfig.Unlock.Interval * 60) + 10 + # unlock the vault, and set password + $VaultConfig | Unlock-PodeSecretManagementVault + + # set the password timeout for the vault + if (!$secretStoreExists) { + if ($VaultConfig.Parameters.PasswordTimeout -le 0) { + $vaultParams['PasswordTimeout'] = ($VaultConfig.Unlock.Interval * 60) + 10 + } } - } - # set config - $null = Set-SecretStoreConfiguration @vaultParams -Confirm:$false -ErrorAction Stop + # set config + $null = Set-SecretStoreConfiguration @vaultParams -Confirm:$false -ErrorAction Stop + } } } function Register-PodeSecretCustomVault { param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [hashtable] $VaultConfig, @@ -142,78 +156,84 @@ function Register-PodeSecretCustomVault { [scriptblock] $UnregisterScriptBlock ) + process { + # unlock secret with no script? + if ($VaultConfig.Unlock.Enabled -and (Test-PodeIsEmpty $UnlockScriptBlock)) { + # Unlock secret supplied for custom Secret Vault type, but not Unlock ScriptBlock supplied + throw ($PodeLocale.unlockSecretButNoScriptBlockExceptionMessage) + } - # unlock secret with no script? - if ($VaultConfig.Unlock.Enabled -and (Test-PodeIsEmpty $UnlockScriptBlock)) { - throw 'Unlock secret supplied for custom Secret Vault type, but not Unlock ScriptBlock supplied' - } - - # all is good, so set the config - $VaultConfig['Custom'] = @{ - Read = $ScriptBlock - Unlock = $UnlockScriptBlock - Remove = $RemoveScriptBlock - Set = $SetScriptBlock - Unregister = $UnregisterScriptBlock + # all is good, so set the config + $VaultConfig['Custom'] = @{ + Read = $ScriptBlock + Unlock = $UnlockScriptBlock + Remove = $RemoveScriptBlock + Set = $SetScriptBlock + Unregister = $UnregisterScriptBlock + } } } function Unlock-PodeSecretManagementVault { param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [hashtable] $VaultConfig ) - # do we need to unlock the vault? - if (!$VaultConfig.Unlock.Enabled) { - return $null - } + process { + # do we need to unlock the vault? + if (!$VaultConfig.Unlock.Enabled) { + return $null + } - # unlock the vault - $null = Unlock-SecretVault -Name $VaultConfig.SecretManagement.VaultName -Password $VaultConfig.Unlock.Secret -ErrorAction Stop + # unlock the vault + $null = Unlock-SecretVault -Name $VaultConfig.SecretManagement.VaultName -Password $VaultConfig.Unlock.Secret -ErrorAction Stop - # interval? - if ($VaultConfig.Unlock.Interval -gt 0) { - return ([datetime]::UtcNow.AddMinutes($VaultConfig.Unlock.Interval)) - } + # interval? + if ($VaultConfig.Unlock.Interval -gt 0) { + return ([datetime]::UtcNow.AddMinutes($VaultConfig.Unlock.Interval)) + } - return $null + return $null + } } function Unlock-PodeSecretCustomVault { param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [hashtable] $VaultConfig ) + process { + # do we need to unlock the vault? + if (!$VaultConfig.Unlock.Enabled) { + return + } - # do we need to unlock the vault? - if (!$VaultConfig.Unlock.Enabled) { - return - } - - # do we have an unlock scriptblock - if ($null -eq $VaultConfig.Custom.Unlock) { - throw "No Unlock ScriptBlock supplied for unlocking the vault '$($VaultConfig.Name)'" - } + # do we have an unlock scriptblock + if ($null -eq $VaultConfig.Custom.Unlock) { + # No Unlock ScriptBlock supplied for unlocking the vault '$($VaultConfig.Name)' + throw ($PodeLocale.noUnlockScriptBlockForVaultExceptionMessage -f $VaultConfig.Name) + } - # unlock the vault, and get back an expiry - $expiry = Invoke-PodeScriptBlock -ScriptBlock $VaultConfig.Custom.Unlock -Splat -Return -Arguments @( - $VaultConfig.Parameters, + # unlock the vault, and get back an expiry + $expiry = Invoke-PodeScriptBlock -ScriptBlock $VaultConfig.Custom.Unlock -Splat -Return -Arguments @( + $VaultConfig.Parameters, (ConvertFrom-SecureString -SecureString $VaultConfig.Unlock.Secret -AsPlainText) - ) + ) - # return expiry if given, otherwise check interval - if ($null -ne $expiry) { - return $expiry - } + # return expiry if given, otherwise check interval + if ($null -ne $expiry) { + return $expiry + } - if ($VaultConfig.Unlock.Interval -gt 0) { - return ([datetime]::UtcNow.AddMinutes($VaultConfig.Unlock.Interval)) - } + if ($VaultConfig.Unlock.Interval -gt 0) { + return ([datetime]::UtcNow.AddMinutes($VaultConfig.Unlock.Interval)) + } - return $null + return $null + } } function Unregister-PodeSecretManagementVault { @@ -222,14 +242,15 @@ function Unregister-PodeSecretManagementVault { [hashtable] $VaultConfig ) + process { + # do we need to unregister the vault? + if ($VaultConfig.AutoImported) { + return + } - # do we need to unregister the vault? - if ($VaultConfig.AutoImported) { - return + # unregister the vault + $null = Unregister-SecretVault -Name $VaultConfig.SecretManagement.VaultName -Confirm:$false -ErrorAction Stop } - - # unregister the vault - $null = Unregister-SecretVault -Name $VaultConfig.SecretManagement.VaultName -Confirm:$false -ErrorAction Stop } function Unregister-PodeSecretCustomVault { @@ -238,21 +259,22 @@ function Unregister-PodeSecretCustomVault { [hashtable] $VaultConfig ) + process { + # do we need to unregister the vault? + if ($VaultConfig.AutoImported) { + return + } - # do we need to unregister the vault? - if ($VaultConfig.AutoImported) { - return - } + # do we have an unregister scriptblock? if not, just do nothing + if ($null -eq $VaultConfig.Custom.Unregister) { + return + } - # do we have an unregister scriptblock? if not, just do nothing - if ($null -eq $VaultConfig.Custom.Unregister) { - return + # unregister the vault + $null = Invoke-PodeScriptBlock -ScriptBlock $VaultConfig.Custom.Unregister -Splat -Arguments @( + $VaultConfig.Parameters + ) } - - # unregister the vault - $null = Invoke-PodeScriptBlock -ScriptBlock $VaultConfig.Custom.Unregister -Splat -Arguments @( - $VaultConfig.Parameters - ) } function Get-PodeSecretManagementKey { @@ -352,7 +374,7 @@ function Set-PodeSecretCustomKey { # do we have a set scriptblock? if ($null -eq $_vault.Custom.Set) { - throw "No Set ScriptBlock supplied for updating/creating secrets in the vault '$($_vault.Name)'" + throw ($PodeLocale.noSetScriptBlockForVaultExceptionMessage -f $_vault.Name) #"No Set ScriptBlock supplied for updating/creating secrets in the vault '$($_vault.Name)'" } # set the secret @@ -402,7 +424,7 @@ function Remove-PodeSecretCustomKey { # do we have a remove scriptblock? if ($null -eq $_vault.Custom.Remove) { - throw "No Remove ScriptBlock supplied for removing secrets from the vault '$($_vault.Name)'" + throw ($PodeLocale.noRemoveScriptBlockForVaultExceptionMessage -f $_vault.Name) #"No Remove ScriptBlock supplied for removing secrets from the vault '$($_vault.Name)'" } # remove the secret @@ -449,16 +471,46 @@ function Start-PodeSecretVaultUnlocker { } } -function Unregister-PodeSecretVaults { +<# +.SYNOPSIS + Unregisters multiple secret vaults within Pode. + +.DESCRIPTION + The `Unregister-PodeSecretVaultsInternal` function iterates through the list of secret vaults + stored in the PodeContext and unregisters each one. If an error occurs during unregistration, + it can either throw an exception or log the error. + +.PARAMETER ThrowError + If specified, the function will throw an exception when an error occurs during unregistration. + Otherwise, it will log the error and continue processing. + +.INPUTS + None. You cannot pipe objects to Unregister-PodeSecretVaultsInternal. + +.OUTPUTS + None. The function modifies the state of secret vaults in the PodeContext. + +.EXAMPLE + # Example usage: + Unregister-PodeSecretVaultsInternal -ThrowError + # All registered secret vaults are unregistered, and any errors are thrown as exceptions. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Unregister-PodeSecretVaultsInternal { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] param( [switch] $ThrowError ) + # Check if there are any secret vaults to unregister if (Test-PodeIsEmpty $PodeContext.Server.Secrets.Vaults) { return } + # Iterate through each vault and attempt unregistration foreach ($vault in $PodeContext.Server.Secrets.Vaults.Values.Name) { if ([string]::IsNullOrEmpty($vault)) { continue @@ -493,7 +545,7 @@ function Protect-PodeSecretValueType { $Value = [string]::Empty } - if ($Value -is [ordered]) { + if ($Value -is [System.Collections.Specialized.OrderedDictionary]) { $Value = [hashtable]$Value } @@ -505,7 +557,7 @@ function Protect-PodeSecretValueType { ($Value -is [pscredential]) -or ($Value -is [System.Management.Automation.OrderedHashtable]) )) { - throw "Value to set secret to is of an invalid type. Expected either String, SecureString, HashTable, Byte[], or PSCredential. But got: $($Value.GetType().Name)" + throw ($PodeLocale.invalidSecretValueTypeExceptionMessage -f $Value.GetType().Name) #"Value to set secret to is of an invalid type. Expected either String, SecureString, HashTable, Byte[], or PSCredential. But got: $($Value.GetType().Name)" } return $Value diff --git a/src/Private/Security.ps1 b/src/Private/Security.ps1 index 559f672d0..c5f475db4 100644 --- a/src/Private/Security.ps1 +++ b/src/Private/Security.ps1 @@ -180,6 +180,30 @@ function Test-PodeRouteLimit { } } +<# +.SYNOPSIS +Checks if a given endpoint has exceeded its limit according to the defined rate limiting rules in Pode. + +.DESCRIPTION +This function evaluates the rate limiting rules for a specified endpoint and determines if the endpoint is allowed to proceed based on the defined limits and the current usage rate. If the endpoint is not active or not defined in the rules, it is either allowed by default or added to the active list with its respective rule. + +.PARAMETER EndpointName +The name of the endpoint to check against the rate limiting rules. + +.EXAMPLE +Test-PodeEndpointLimit -EndpointName "MyEndpoint" +Checks if "MyEndpoint" is allowed to proceed based on the current rate limiting rules. + +.EXAMPLE +$result = Test-PodeEndpointLimit -EndpointName $null +Checks if an unnamed endpoint (e.g., $null) is allowed, which always returns $true. + +.RETURNS +[boolean] - Returns $true if the endpoint is allowed, otherwise $false. + +.NOTES +This is an internal function and may change in future releases of Pode. +#> function Test-PodeEndpointLimit { param( [Parameter()] @@ -348,11 +372,11 @@ function Add-PodeIPLimit { # ensure limit and seconds are non-zero and negative if ($Limit -le 0) { - throw "Limit value cannot be 0 or less for $($IP)" + throw ($PodeLocale.limitValueCannotBeZeroOrLessExceptionMessage -f $IP) #"Limit value cannot be 0 or less for $($IP)" } if ($Seconds -le 0) { - throw "Seconds value cannot be 0 or less for $($IP)" + throw ($PodeLocale.secondsValueCannotBeZeroOrLessExceptionMessage -f $IP) #"Seconds value cannot be 0 or less for $($IP)" } # get current rules @@ -426,11 +450,11 @@ function Add-PodeRouteLimit { # ensure limit and seconds are non-zero and negative if ($Limit -le 0) { - throw "Limit value cannot be 0 or less for $($IP)" + throw ($PodeLocale.limitValueCannotBeZeroOrLessExceptionMessage -f $IP) #"Limit value cannot be 0 or less for $($IP)" } if ($Seconds -le 0) { - throw "Seconds value cannot be 0 or less for $($IP)" + throw ($PodeLocale.secondsValueCannotBeZeroOrLessExceptionMessage -f $IP) #"Seconds value cannot be 0 or less for $($IP)" } # get current rules @@ -482,16 +506,16 @@ function Add-PodeEndpointLimit { # does the endpoint exist? $endpoint = Get-PodeEndpointByName -Name $EndpointName if ($null -eq $endpoint) { - throw "Endpoint not found: $($EndpointName)" + throw ($PodeLocale.endpointNameNotExistExceptionMessage -f $EndpointName) #"Endpoint not found: $($EndpointName)" } # ensure limit and seconds are non-zero and negative if ($Limit -le 0) { - throw "Limit value cannot be 0 or less for $($IP)" + throw ($PodeLocale.limitValueCannotBeZeroOrLessExceptionMessage -f $IP) #"Limit value cannot be 0 or less for $($IP)" } if ($Seconds -le 0) { - throw "Seconds value cannot be 0 or less for $($IP)" + throw ($PodeLocale.secondsValueCannotBeZeroOrLessExceptionMessage -f $IP) #"Seconds value cannot be 0 or less for $($IP)" } # get current rules @@ -816,7 +840,7 @@ function Get-PodeCertificateByPemFile { $result = openssl pkcs12 -inkey $keyPath -in $certPath -export -passin pass:$Password -password pass:$Password -out $tempFile if (!$?) { - throw "Failed to create openssl cert: $($result)" + throw ($PodeLocale.failedToCreateOpenSslCertExceptionMessage -f $result) #"Failed to create openssl cert: $($result)" } $cert = [X509Certificates.X509Certificate2]::new($tempFile, $Password) @@ -850,7 +874,8 @@ function Find-PodeCertificateInCertStore { # fail if not windows if (!(Test-PodeIsWindows)) { - throw 'Certificate Thumbprints/Name are only supported on Windows' + # Certificate Thumbprints/Name are only supported on Windows + throw ($PodeLocale.certificateThumbprintsNameSupportedOnWindowsExceptionMessage) } # open the currentuser\my store @@ -870,7 +895,7 @@ function Find-PodeCertificateInCertStore { # fail if no cert found for query if (($null -eq $x509certs) -or ($x509certs.Count -eq 0)) { - throw "No certificate could be found in $($StoreLocation)\$($StoreName) for '$($Query)'" + throw ($PodeLocale.noCertificateFoundExceptionMessage -f $StoreLocation, $StoreName, $Query) # "No certificate could be found in $($StoreLocation)\$($StoreName) for '$($Query)'" } return ([X509Certificates.X509Certificate2]($x509certs[0])) @@ -1094,4 +1119,187 @@ function Protect-PodePermissionsPolicyKeyword { }) return "$($Name)=($($values -join ' '))" +} + +<# +.SYNOPSIS +Sets the Content Security Policy (CSP) header for a Pode web server. + +.DESCRIPTION +The `Set-PodeSecurityContentSecurityPolicyInternal` function constructs and sets the Content Security Policy (CSP) header based on the provided parameters. The function supports an optional switch to append the header value and explicitly disables XSS auditors in modern browsers to prevent vulnerabilities. + +.PARAMETER Params +A hashtable containing the various CSP directives to be set. + +.PARAMETER Append +A switch indicating whether to append the header value. + +.EXAMPLE +$policyParams = @{ + Default = "'self'" + ScriptSrc = "'self' 'unsafe-inline'" + StyleSrc = "'self' 'unsafe-inline'" +} +Set-PodeSecurityContentSecurityPolicyInternal -Params $policyParams + +.EXAMPLE +$policyParams = @{ + Default = "'self'" + ImgSrc = "'self' data:" + ConnectSrc = "'self' https://api.example.com" + UpgradeInsecureRequests = $true +} +Set-PodeSecurityContentSecurityPolicyInternal -Params $policyParams -Append + +.NOTES +This is an internal function and may change in future releases of Pode. +#> +function Set-PodeSecurityContentSecurityPolicyInternal { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSPossibleIncorrectComparisonWithNull', '')] + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [hashtable] + $Params, + + [Parameter()] + [switch] + $Append + ) + + # build the header's value + $values = @( + Protect-PodeContentSecurityKeyword -Name 'default-src' -Value $Params.Default -Append:$Append + Protect-PodeContentSecurityKeyword -Name 'child-src' -Value $Params.Child -Append:$Append + Protect-PodeContentSecurityKeyword -Name 'connect-src' -Value $Params.Connect -Append:$Append + Protect-PodeContentSecurityKeyword -Name 'font-src' -Value $Params.Font -Append:$Append + Protect-PodeContentSecurityKeyword -Name 'frame-src' -Value $Params.Frame -Append:$Append + Protect-PodeContentSecurityKeyword -Name 'img-src' -Value $Params.Image -Append:$Append + Protect-PodeContentSecurityKeyword -Name 'manifest-src' -Value $Params.Manifest -Append:$Append + Protect-PodeContentSecurityKeyword -Name 'media-src' -Value $Params.Media -Append:$Append + Protect-PodeContentSecurityKeyword -Name 'object-src' -Value $Params.Object -Append:$Append + Protect-PodeContentSecurityKeyword -Name 'script-src' -Value $Params.Scripts -Append:$Append + Protect-PodeContentSecurityKeyword -Name 'style-src' -Value $Params.Style -Append:$Append + Protect-PodeContentSecurityKeyword -Name 'base-uri' -Value $Params.BaseUri -Append:$Append + Protect-PodeContentSecurityKeyword -Name 'form-action' -Value $Params.FormAction -Append:$Append + Protect-PodeContentSecurityKeyword -Name 'frame-ancestors' -Value $Params.FrameAncestor -Append:$Append + ) + + if (![string]::IsNullOrWhiteSpace($Params.Sandbox) -and ($Params.Sandbox -ine 'None')) { + $values += "sandbox $($Params.Sandbox.ToLowerInvariant())".Trim() + } + + if ($Params.UpgradeInsecureRequests) { + $values += 'upgrade-insecure-requests' + } + + # Filter out $null values from the $values array using the array filter `-ne $null`. This approach + # is equivalent to using `$values | Where-Object { $_ -ne $null }` but is more efficient. The `-ne $null` + # operator is faster because it is a direct array operation that internally skips the overhead of + # piping through a cmdlet and processing each item individually. + $values = ($values -ne $null) + $value = ($values -join '; ') + + # Add the Content Security Policy header to the response or relevant context. This cmdlet + # sets the HTTP header with the name 'Content-Security-Policy' and the constructed value. + Add-PodeSecurityHeader -Name 'Content-Security-Policy' -Value $value + + # this is done to explicitly disable XSS auditors in modern browsers + # as having it enabled has now been found to cause more vulnerabilities + if ($Params.XssBlock) { + Add-PodeSecurityHeader -Name 'X-XSS-Protection' -Value '1; mode=block' + } + else { + Add-PodeSecurityHeader -Name 'X-XSS-Protection' -Value '0' + } +} + +<# +.SYNOPSIS +Sets the Permissions Policy header for a Pode web server. + +.DESCRIPTION +The `Set-PodeSecurityPermissionsPolicy` function constructs and sets the Permissions Policy header based on the provided parameters. The function supports an optional switch to append the header value. + +.PARAMETER Params +A hashtable containing the various permissions policies to be set. + +.PARAMETER Append +A switch indicating whether to append the header value. + +.EXAMPLE +$policyParams = @{ + Accelerometer = 'none' + Camera = 'self' + Microphone = '*' +} +Set-PodeSecurityPermissionsPolicy -Params $policyParams + +.EXAMPLE +$policyParams = @{ + Autoplay = 'self' + Geolocation = 'none' +} +Set-PodeSecurityPermissionsPolicy -Params $policyParams -Append + +.NOTES +This is an internal function and may change in future releases of Pode. +#> +function Set-PodeSecurityPermissionsPolicyInternal { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSPossibleIncorrectComparisonWithNull', '')] + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [hashtable] + $Params, + + [Parameter()] + [switch] + $Append + ) + + # build the header's value + $values = @( + Protect-PodePermissionsPolicyKeyword -Name 'accelerometer' -Value $Params.Accelerometer -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'ambient-light-sensor' -Value $Params.AmbientLightSensor -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'autoplay' -Value $Params.Autoplay -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'battery' -Value $Params.Battery -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'camera' -Value $Params.Camera -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'display-capture' -Value $Params.DisplayCapture -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'document-domain' -Value $Params.DocumentDomain -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'encrypted-media' -Value $Params.EncryptedMedia -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'fullscreen' -Value $Params.Fullscreen -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'gamepad' -Value $Params.Gamepad -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'geolocation' -Value $Params.Geolocation -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'gyroscope' -Value $Params.Gyroscope -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'interest-cohort' -Value $Params.InterestCohort -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'layout-animations' -Value $Params.LayoutAnimations -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'legacy-image-formats' -Value $Params.LegacyImageFormats -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'magnetometer' -Value $Params.Magnetometer -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'microphone' -Value $Params.Microphone -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'midi' -Value $Params.Midi -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'oversized-images' -Value $Params.OversizedImages -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'payment' -Value $Params.Payment -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'picture-in-picture' -Value $Params.PictureInPicture -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'publickey-credentials-get' -Value $Params.PublicKeyCredentials -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'speaker-selection' -Value $Params.Speakers -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'sync-xhr' -Value $Params.SyncXhr -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'unoptimized-images' -Value $Params.UnoptimisedImages -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'unsized-media' -Value $Params.UnsizedMedia -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'usb' -Value $Params.Usb -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'screen-wake-lock' -Value $Params.ScreenWakeLake -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'web-share' -Value $Params.WebShare -Append:$Append + Protect-PodePermissionsPolicyKeyword -Name 'xr-spatial-tracking' -Value $Params.XrSpatialTracking -Append:$Append + ) + + # Filter out $null values from the $values array using the array filter `-ne $null`. This approach + # is equivalent to using `$values | Where-Object { $_ -ne $null }` but is more efficient. The `-ne $null` + # operator is faster because it is a direct array operation that internally skips the overhead of + # piping through a cmdlet and processing each item individually. + $values = ($values -ne $null) + $value = ($values -join ', ') + + # Add the constructed Permissions Policy header to the response or relevant context. This cmdlet + # sets the HTTP header with the name 'Permissions-Policy' and the constructed value. + Add-PodeSecurityHeader -Name 'Permissions-Policy' -Value $value } \ No newline at end of file diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index 367d065fd..ea4fa1f87 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -59,19 +59,19 @@ function Start-PodeInternalServer { New-PodeAutoRestartServer # start the runspace pools for web, schedules, etc - New-PodeRunspacePools - Open-PodeRunspacePools + New-PodeRunspacePool + Open-PodeRunspacePool if (!$PodeContext.Server.IsServerless) { # start runspace for loggers Start-PodeLoggingRunspace - # start runspace for timers - Start-PodeTimerRunspace - # start runspace for schedules Start-PodeScheduleRunspace + # start runspace for timers + Start-PodeTimerRunspace + # start runspace for gui Start-PodeGuiRunspace @@ -136,7 +136,7 @@ function Start-PodeInternalServer { # errored? if ($PodeContext.RunspacePools[$pool].State -ieq 'error') { - throw "$($pool) RunspacePool failed to load" + throw ($PodeLocale.runspacePoolFailedToLoadExceptionMessage -f $pool) #"$($pool) RunspacePool failed to load" } } } @@ -149,7 +149,9 @@ function Start-PodeInternalServer { # state what endpoints are being listened on if ($endpoints.Length -gt 0) { - Write-PodeHost "Listening on the following $($endpoints.Length) endpoint(s) [$($PodeContext.Threads.General) thread(s)]:" -ForegroundColor Yellow + + # Listening on the following $endpoints.Length endpoint(s) [$PodeContext.Threads.General thread(s)] + Write-PodeHost ($PodeLocale.listeningOnEndpointsMessage -f $endpoints.Length, $PodeContext.Threads.General) -ForegroundColor Yellow $endpoints | ForEach-Object { $flags = @() if ($_.DualMode) { @@ -171,28 +173,32 @@ function Start-PodeInternalServer { if ( $bookmarks) { Write-PodeHost if (!$OpenAPIHeader) { - Write-PodeHost 'OpenAPI Info:' -ForegroundColor Yellow + # OpenAPI Info + Write-PodeHost $PodeLocale.openApiInfoMessage -ForegroundColor Yellow $OpenAPIHeader = $true } Write-PodeHost " '$key':" -ForegroundColor Yellow if ($bookmarks.route.count -gt 1 -or $bookmarks.route.Endpoint.Name) { - Write-PodeHost ' - Specification:' -ForegroundColor Yellow + # Specification + Write-PodeHost " - $($PodeLocale.specificationMessage):" -ForegroundColor Yellow foreach ($endpoint in $bookmarks.route.Endpoint) { Write-PodeHost " . $($endpoint.Protocol)://$($endpoint.Address)$($bookmarks.openApiUrl)" -ForegroundColor Yellow } - Write-PodeHost ' - Documentation:' -ForegroundColor Yellow + # Documentation + Write-PodeHost " - $($PodeLocale.documentationMessage):" -ForegroundColor Yellow foreach ($endpoint in $bookmarks.route.Endpoint) { Write-PodeHost " . $($endpoint.Protocol)://$($endpoint.Address)$($bookmarks.path)" -ForegroundColor Yellow } } else { - Write-PodeHost ' - Specification:' -ForegroundColor Yellow + # Specification + Write-PodeHost " - $($PodeLocale.specificationMessage):" -ForegroundColor Yellow $endpoints | ForEach-Object { $url = [System.Uri]::new( [System.Uri]::new($_.Url), $bookmarks.openApiUrl) Write-PodeHost " . $url" -ForegroundColor Yellow } - Write-PodeHost ' - Documentation:' -ForegroundColor Yellow + Write-PodeHost " - $($PodeLocale.documentationMessage):" -ForegroundColor Yellow $endpoints | ForEach-Object { $url = [System.Uri]::new( [System.Uri]::new($_.Url), $bookmarks.path) Write-PodeHost " . $url" -ForegroundColor Yellow @@ -211,7 +217,8 @@ function Start-PodeInternalServer { function Restart-PodeInternalServer { try { # inform restart - Write-PodeHost 'Restarting server...' -NoNewline -ForegroundColor Cyan + # Restarting server... + Write-PodeHost $PodeLocale.restartingServerMessage -NoNewline -ForegroundColor Cyan # run restart event hooks Invoke-PodeEvent -Type Restart @@ -220,18 +227,18 @@ function Restart-PodeInternalServer { $PodeContext.Tokens.Cancellation.Cancel() # close all current runspaces - Close-PodeRunspaces -ClosePool + Close-PodeRunspace -ClosePool # remove all of the pode temp drives - Remove-PodePSDrives + Remove-PodePSDrive # clear-up modules $PodeContext.Server.Modules.Clear() # clear up timers, schedules and loggers - $PodeContext.Server.Routes | Clear-PodeHashtableInnerKeys - $PodeContext.Server.Handlers | Clear-PodeHashtableInnerKeys - $PodeContext.Server.Events | Clear-PodeHashtableInnerKeys + Clear-PodeHashtableInnerKey -InputObject $PodeContext.Server.Routes + Clear-PodeHashtableInnerKey -InputObject $PodeContext.Server.Handlers + Clear-PodeHashtableInnerKey -InputObject $PodeContext.Server.Events if ($null -ne $PodeContext.Server.Verbs) { $PodeContext.Server.Verbs.Clear() @@ -247,7 +254,7 @@ function Restart-PodeInternalServer { # clear tasks $PodeContext.Tasks.Items.Clear() - $PodeContext.Tasks.Results.Clear() + $PodeContext.Tasks.Processes.Clear() # clear file watchers $PodeContext.Fim.Items.Clear() @@ -264,7 +271,7 @@ function Restart-PodeInternalServer { # clear security headers $PodeContext.Server.Security.Headers.Clear() - $PodeContext.Server.Security.Cache | Clear-PodeHashtableInnerKeys + Clear-PodeHashtableInnerKey -InputObject $PodeContext.Server.Security.Cache # clear endpoints $PodeContext.Server.Endpoints.Clear() @@ -307,7 +314,7 @@ function Restart-PodeInternalServer { $PodeContext.Server.Cache.Storage.Clear() # clear up secret vaults/cache - Unregister-PodeSecretVaults -ThrowError + Unregister-PodeSecretVaultsInternal -ThrowError $PodeContext.Server.Secrets.Vaults.Clear() $PodeContext.Server.Secrets.Keys.Clear() @@ -324,16 +331,16 @@ function Restart-PodeInternalServer { # recreate the session tokens Close-PodeDisposable -Disposable $PodeContext.Tokens.Cancellation - $PodeContext.Tokens.Cancellation = New-Object System.Threading.CancellationTokenSource + $PodeContext.Tokens.Cancellation = [System.Threading.CancellationTokenSource]::new() Close-PodeDisposable -Disposable $PodeContext.Tokens.Restart - $PodeContext.Tokens.Restart = New-Object System.Threading.CancellationTokenSource + $PodeContext.Tokens.Restart = [System.Threading.CancellationTokenSource]::new() # reload the configuration $PodeContext.Server.Configuration = Open-PodeConfiguration -Context $PodeContext # done message - Write-PodeHost ' Done' -ForegroundColor Green + Write-PodeHost $PodeLocale.doneMessage -ForegroundColor Green # restart the server $PodeContext.Metrics.Server.RestartCount++ diff --git a/src/Private/Serverless.ps1 b/src/Private/Serverless.ps1 index a7805947c..a0cebb6b1 100644 --- a/src/Private/Serverless.ps1 +++ b/src/Private/Serverless.ps1 @@ -1,4 +1,5 @@ function Start-PodeAzFuncServer { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] param( [Parameter(Mandatory = $true)] $Data @@ -21,7 +22,7 @@ function Start-PodeAzFuncServer { $request = $Data.Request # setup the response - $response = New-Object -TypeName HttpResponseContext + $response = New-PodeAzFuncResponse $response.StatusCode = 200 $response.Headers = @{} @@ -108,7 +109,7 @@ function Start-PodeAzFuncServer { Set-PodeResponseStatus -Code 500 -Exception $_ } finally { - Update-PodeServerRequestMetrics -WebEvent $WebEvent + Update-PodeServerRequestMetric -WebEvent $WebEvent } # invoke endware specifc to the current web event @@ -124,7 +125,12 @@ function Start-PodeAzFuncServer { } } +function New-PodeAzFuncResponse { + return [HttpResponseContext]::new() +} + function Start-PodeAwsLambdaServer { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] param( [Parameter(Mandatory = $true)] $Data @@ -223,7 +229,7 @@ function Start-PodeAwsLambdaServer { Set-PodeResponseStatus -Code 500 -Exception $_ } finally { - Update-PodeServerRequestMetrics -WebEvent $WebEvent + Update-PodeServerRequestMetric -WebEvent $WebEvent } # invoke endware specifc to the current web event diff --git a/src/Private/ServiceServer.ps1 b/src/Private/ServiceServer.ps1 index 9f39bb535..bd7fe6eca 100644 --- a/src/Private/ServiceServer.ps1 +++ b/src/Private/ServiceServer.ps1 @@ -1,18 +1,21 @@ function Start-PodeServiceServer { # ensure we have service handlers if (Test-PodeIsEmpty (Get-PodeHandler -Type Service)) { - throw 'No Service handlers have been defined' + # No Service handlers have been defined + throw ($PodeLocale.noServiceHandlersDefinedExceptionMessage) } # state we're running - Write-PodeHost "Server looping every $($PodeContext.Server.Interval)secs" -ForegroundColor Yellow + # Server looping every $PodeContext.Server.Interval secs + Write-PodeHost ($PodeLocale.serverLoopingMessage -f $PodeContext.Server.Interval) -ForegroundColor Yellow # script for the looping server $serverScript = { + try { while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { # the event object - $ServiceEvent = @{ + $script:ServiceEvent = @{ Lockable = $PodeContext.Threading.Lockables.Global Metadata = @{} } @@ -28,7 +31,9 @@ function Start-PodeServiceServer { Start-Sleep -Seconds $PodeContext.Server.Interval } } - catch [System.OperationCanceledException] {} + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } catch { $_ | Write-PodeErrorLog throw $_.Exception @@ -36,5 +41,5 @@ function Start-PodeServiceServer { } # start the runspace for the server - Add-PodeRunspace -Type Main -ScriptBlock $serverScript + Add-PodeRunspace -Type Main -Name 'ServiceServer' -ScriptBlock $serverScript } \ No newline at end of file diff --git a/src/Private/Sessions.ps1 b/src/Private/Sessions.ps1 index 35445d1cc..4e9bc870c 100644 --- a/src/Private/Sessions.ps1 +++ b/src/Private/Sessions.ps1 @@ -40,7 +40,8 @@ function Get-PodeSessionFullId { function Set-PodeSession { if ($null -eq $WebEvent.Session) { - throw 'there is no session available to set on the response' + # There is no session available to set on the response + throw ($PodeLocale.noSessionToSetOnResponseExceptionMessage) } # convert secret to strict mode @@ -137,7 +138,8 @@ function Revoke-PodeSession { function Set-PodeSessionDataHash { if ($null -eq $WebEvent.Session) { - throw 'No session available to calculate data hash' + # No session available to calculate data hash + throw ($PodeLocale.noSessionToCalculateDataHashExceptionMessage) } if (($null -eq $WebEvent.Session.Data) -or ($WebEvent.Session.Data.Count -eq 0)) { @@ -230,7 +232,7 @@ function Remove-PodeSessionInternal { } function Get-PodeSessionInMemStore { - $store = New-Object -TypeName psobject + $store = [psobject]::new() # add in-mem storage $store | Add-Member -MemberType NoteProperty -Name Memory -Value @{} diff --git a/src/Private/Setup.ps1 b/src/Private/Setup.ps1 index 340706864..d48ff1f7b 100644 --- a/src/Private/Setup.ps1 +++ b/src/Private/Setup.ps1 @@ -1,4 +1,5 @@ function Invoke-PodePackageScript { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingInvokeExpression', '')] param( [Parameter()] [string] @@ -12,30 +13,44 @@ function Invoke-PodePackageScript { Invoke-Expression -Command $ActionScript } -function Install-PodeLocalModules { +<# +.SYNOPSIS + Installs a local Pode module. + +.DESCRIPTION + This function installs a local Pode module by downloading it from the specified repository. It checks the module version and retrieves the latest version if 'latest' is specified. The module is saved to the specified path. + +.PARAMETER Module + The Pode module to install. It should include the module name, version, and repository information. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Install-PodeLocalModule { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] param( [Parameter()] - $Modules = $null + $Module = $null ) - if ($null -eq $Modules) { + if ($null -eq $Module) { return } $psModules = './ps_modules' # download modules to ps_modules - $Modules.psobject.properties.name | ForEach-Object { + $Module.psobject.properties.name | ForEach-Object { $_name = $_ # get the module version - $_version = $Modules.$_name.version + $_version = $Module.$_name.version if ([string]::IsNullOrWhiteSpace($_version)) { - $_version = $Modules.$_name + $_version = $Module.$_name } # get the module repository - $_repository = Protect-PodeValue -Value $Modules.$_name.repository -Default 'PSGallery' + $_repository = Protect-PodeValue -Value $Module.$_name.repository -Default 'PSGallery' try { # if version is latest, retrieve current @@ -60,7 +75,7 @@ function Install-PodeLocalModules { } catch { Write-Host 'Failed' -ForegroundColor Red - throw "Module or version not found on $($_repository): $($_name)@$($_version)" + throw ($PodeLocale.moduleOrVersionNotFoundExceptionMessage -f $_repository, $_name, $_version) #"Module or version not found on $($_repository): $($_name)@$($_version)" } } } \ No newline at end of file diff --git a/src/Private/SmtpServer.ps1 b/src/Private/SmtpServer.ps1 index 603e08dff..c3cc7cfd2 100644 --- a/src/Private/SmtpServer.ps1 +++ b/src/Private/SmtpServer.ps1 @@ -3,13 +3,14 @@ using namespace Pode function Start-PodeSmtpServer { # ensure we have smtp handlers if (Test-PodeIsEmpty (Get-PodeHandler -Type Smtp)) { - throw 'No SMTP handlers have been defined' + # No SMTP handlers have been defined + throw ($PodeLocale.noSmtpHandlersDefinedExceptionMessage) } # work out which endpoints to listen on $endpoints = @() - @(Get-PodeEndpoints -Type Smtp) | ForEach-Object { + @(Get-PodeEndpointByProtocolType -Type Smtp) | ForEach-Object { # get the ip address $_ip = [string]($_.Address) $_ip = Get-PodeIPAddressesForHostname -Hostname $_ip -Type All | Select-Object -First 1 @@ -48,7 +49,7 @@ function Start-PodeSmtpServer { # create the listener $listener = [PodeListener]::new($PodeContext.Tokens.Cancellation.Token) $listener.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled) - $listener.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevels) + $listener.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevel) $listener.RequestTimeout = $PodeContext.Server.Request.Timeout $listener.RequestBodySize = $PodeContext.Server.Request.BodySize @@ -98,7 +99,7 @@ function Start-PodeSmtpServer { $Request = $context.Request $Response = $context.Response - $SmtpEvent = @{ + $script:SmtpEvent = @{ Response = $Response Request = $Request Lockable = $PodeContext.Threading.Lockables.Global @@ -150,19 +151,23 @@ function Start-PodeSmtpServer { } } } - catch [System.OperationCanceledException] {} + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } catch { $_ | Write-PodeErrorLog $_.Exception | Write-PodeErrorLog -CheckInnerException } } finally { - $SmtpEvent = $null + $script:SmtpEvent = $null Close-PodeDisposable -Disposable $context } } } - catch [System.OperationCanceledException] {} + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } catch { $_ | Write-PodeErrorLog $_.Exception | Write-PodeErrorLog -CheckInnerException @@ -172,7 +177,7 @@ function Start-PodeSmtpServer { # start the runspace for listening on x-number of threads 1..$PodeContext.Threads.General | ForEach-Object { - Add-PodeRunspace -Type Smtp -ScriptBlock $listenScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ } + Add-PodeRunspace -Type Smtp -Name 'Listener' -Id $_ -ScriptBlock $listenScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ } } # script to keep smtp server listening until cancelled @@ -188,7 +193,9 @@ function Start-PodeSmtpServer { Start-Sleep -Seconds 1 } } - catch [System.OperationCanceledException] {} + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } catch { $_ | Write-PodeErrorLog $_.Exception | Write-PodeErrorLog -CheckInnerException @@ -199,7 +206,7 @@ function Start-PodeSmtpServer { } } - Add-PodeRunspace -Type Smtp -ScriptBlock $waitScript -Parameters @{ 'Listener' = $listener } -NoProfile + Add-PodeRunspace -Type Smtp -Name 'KeepAlive' -ScriptBlock $waitScript -Parameters @{ 'Listener' = $listener } -NoProfile # state where we're running return @(foreach ($endpoint in $endpoints) { diff --git a/src/Private/Streams.ps1 b/src/Private/Streams.ps1 index 6e44da324..261ca3586 100644 --- a/src/Private/Streams.ps1 +++ b/src/Private/Streams.ps1 @@ -33,7 +33,7 @@ function Read-PodeByteLineFromByteArray { $IncludeNewLine ) - $nlBytes = Get-PodeNewLineBytes -Encoding $Encoding + $nlBytes = Get-PodeNewLineByte -Encoding $Encoding # attempt to find \n $index = [array]::IndexOf($Bytes, $nlBytes.NewLine, $StartIndex) @@ -71,7 +71,7 @@ function Get-PodeByteLinesFromByteArray { # lines $lines = @() - $nlBytes = Get-PodeNewLineBytes -Encoding $Encoding + $nlBytes = Get-PodeNewLineByte -Encoding $Encoding # attempt to find \n $index = 0 @@ -93,26 +93,78 @@ function Get-PodeByteLinesFromByteArray { return $lines } - -function ConvertFrom-PodeStreamToBytes { +<# +.SYNOPSIS + Converts a stream to a byte array. + +.DESCRIPTION + The `ConvertFrom-PodeValueToByteArray` function reads data from a stream and converts it to a byte array. + It's useful for scenarios where you need to work with binary data from a stream. + +.PARAMETER Stream + Specifies the input stream to convert. This parameter is mandatory. + +.OUTPUTS + Returns a byte array containing the data read from the input stream. + +.EXAMPLE + # Example usage: + # Read data from a file stream and convert it to a byte array + $stream = [System.IO.File]::OpenRead("C:\path\to\file.bin") + $byteArray = ConvertFrom-PodeValueToByteArray -Stream $stream + $stream.Close() + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function ConvertFrom-PodeValueToByteArray { param( [Parameter(Mandatory = $true)] $Stream ) + # Initialize a buffer to read data in chunks $buffer = [byte[]]::new(64 * 1024) - $ms = New-Object -TypeName System.IO.MemoryStream + $ms = [System.IO.MemoryStream]::new() $read = 0 + # Read data from the stream and write it to the memory stream while (($read = $Stream.Read($buffer, 0, $buffer.Length)) -gt 0) { $ms.Write($buffer, 0, $read) } + # Close the memory stream and return the byte array $ms.Close() return $ms.ToArray() } - -function ConvertFrom-PodeValueToBytes { +<# +.SYNOPSIS + Converts a string value to a byte array using the specified encoding. + +.DESCRIPTION + The `ConvertFrom-PodeValueToByteArray` function takes a string value and converts it to a byte array. + You can specify the desired encoding (default is UTF-8). + +.PARAMETER Value + Specifies the input string value to convert. + +.PARAMETER Encoding + Specifies the encoding to use when converting the string to bytes. + Default value is UTF-8. + +.OUTPUTS + Returns a byte array containing the encoded representation of the input string. + +.EXAMPLE + # Example usage: + $inputString = "Hello, world!" + $byteArray = ConvertFrom-PodeValueToByteArray -Value $inputString + # Now you can work with the byte array as needed. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function ConvertFrom-PodeValueToByteArray { param( [Parameter()] [string] @@ -150,7 +202,33 @@ function ConvertFrom-PodeBytesToString { return $value } -function Get-PodeNewLineBytes { +<# +.SYNOPSIS + Retrieves information about newline characters in different encodings. + +.DESCRIPTION + The `Get-PodeNewLineByte` function returns a hashtable containing information about newline characters. + It calculates the byte values for newline (`n`) and carriage return (`r`) based on the specified encoding (default is UTF-8). + +.PARAMETER Encoding + Specifies the encoding to use when calculating newline and carriage return byte values. + Default value is UTF-8. + +.OUTPUTS + Returns a hashtable with the following keys: + - `NewLine`: Byte value for newline character (`n`). + - `Return`: Byte value for carriage return character (`r`). + +.EXAMPLE + Get-PodeNewLineByte -Encoding [System.Text.Encoding]::ASCII + # Returns the byte values for newline and carriage return in ASCII encoding. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Get-PodeNewLineByte { + [CmdletBinding()] + [OutputType([hashtable])] param( [Parameter()] $Encoding = [System.Text.Encoding]::UTF8 @@ -199,7 +277,7 @@ function Remove-PodeNewLineBytesFromArray { $Encoding = [System.Text.Encoding]::UTF8 ) - $nlBytes = Get-PodeNewLineBytes -Encoding $Encoding + $nlBytes = Get-PodeNewLineByte -Encoding $Encoding $length = $Bytes.Length - 1 if ($Bytes[$length] -eq $nlBytes.NewLine) { @@ -211,4 +289,38 @@ function Remove-PodeNewLineBytesFromArray { } return $Bytes[0..$length] -} \ No newline at end of file +} + +function Get-PodeCompressionStream { + param ( + [Parameter(Mandatory = $true)] + [System.IO.Stream] + $InputStream, + + [Parameter(Mandatory = $true)] + [ValidateSet('gzip', 'deflate')] + [string] + $Encoding, + + [Parameter(Mandatory = $true)] + [System.IO.Compression.CompressionMode] + $Mode + ) + + $leaveOpen = $Mode -eq [System.IO.Compression.CompressionMode]::Compress + + switch ($Encoding.ToLower()) { + 'gzip' { + return [System.IO.Compression.GZipStream]::new($InputStream, $Mode, $leaveOpen) + } + + 'deflate' { + return [System.IO.Compression.DeflateStream]::new($InputStream, $Mode, $leaveOpen) + } + + default { + # Unsupported stream compression encoding: $Encoding + throw ($PodeLocale.unsupportedStreamCompressionEncodingExceptionMessage -f $Encoding) + } + } +} diff --git a/src/Private/Tasks.ps1 b/src/Private/Tasks.ps1 index 1ab6ad6bb..b723a4f74 100644 --- a/src/Private/Tasks.ps1 +++ b/src/Private/Tasks.ps1 @@ -8,39 +8,38 @@ function Start-PodeTaskHousekeeper { } Add-PodeTimer -Name '__pode_task_housekeeper__' -Interval 30 -ScriptBlock { - if ($PodeContext.Tasks.Results.Count -eq 0) { - return - } - - $now = [datetime]::UtcNow - - foreach ($key in $PodeContext.Tasks.Results.Keys.Clone()) { - $result = $PodeContext.Tasks.Results[$key] - - # has it force expired? - if ($result.ExpireTime -lt $now) { - Close-PodeTaskInternal -Result $result - continue + try { + if ($PodeContext.Tasks.Processes.Count -eq 0) { + return } - # is it completed? - if (!$result.Runspace.Handler.IsCompleted) { - continue + $now = [datetime]::UtcNow + + foreach ($key in $PodeContext.Tasks.Processes.Keys.Clone()) { + try { + $process = $PodeContext.Tasks.Processes[$key] + + # has it completed or expire? then dispose and remove + if ((($null -ne $process.CompletedTime) -and ($process.CompletedTime.AddMinutes(1) -lt $now)) -or ($process.ExpireTime -lt $now)) { + Close-PodeTaskInternal -Process $process + continue + } + + # if completed, and no completed time, set it + if ($process.Runspace.Handler.IsCompleted -and ($null -eq $process.CompletedTime)) { + $process.CompletedTime = $now + } + } + catch { + $_ | Write-PodeErrorLog + } } - # is a completed time set? - if ($null -eq $result.CompletedTime) { - $result.CompletedTime = [datetime]::UtcNow - continue - } - - # is it expired by completion? if so, dispose and remove - if ($result.CompletedTime.AddMinutes(1) -lt $now) { - Close-PodeTaskInternal -Result $result - } + $process = $null + } + catch { + $_ | Write-PodeErrorLog } - - $result = $null } } @@ -48,21 +47,22 @@ function Close-PodeTaskInternal { param( [Parameter()] [hashtable] - $Result + $Process ) - if ($null -eq $Result) { + if ($null -eq $Process) { return } - Close-PodeDisposable -Disposable $Result.Runspace.Pipeline - Close-PodeDisposable -Disposable $Result.Result - $null = $PodeContext.Tasks.Results.Remove($Result.ID) + Close-PodeDisposable -Disposable $Process.Runspace.Pipeline + Close-PodeDisposable -Disposable $Process.Result + $null = $PodeContext.Tasks.Processes.Remove($Process.ID) } function Invoke-PodeInternalTask { param( [Parameter(Mandatory = $true)] + [hashtable] $Task, [Parameter()] @@ -71,66 +71,151 @@ function Invoke-PodeInternalTask { [Parameter()] [int] - $Timeout = -1 + $Timeout = -1, + + [Parameter()] + [ValidateSet('Default', 'Create', 'Start')] + [string] + $TimeoutFrom = 'Default' ) try { + # generate processId for task + $processId = New-PodeGuid + # setup event param $parameters = @{ - Event = @{ - Lockable = $PodeContext.Threading.Lockables.Global - Sender = $Task - Metadata = @{} - } + ProcessId = $processId + ArgumentList = $ArgumentList } - # add any task args - foreach ($key in $Task.Arguments.Keys) { - $parameters[$key] = $Task.Arguments[$key] + # what's the timeout values to use? + if ($TimeoutFrom -eq 'Default') { + $TimeoutFrom = $Task.Timeout.From } - # add adhoc task invoke args - if (($null -ne $ArgumentList) -and ($ArgumentList.Count -gt 0)) { - foreach ($key in $ArgumentList.Keys) { - $parameters[$key] = $ArgumentList[$key] - } + if ($Timeout -eq -1) { + $Timeout = $Task.Timeout.Value } - # add any using variables - if ($null -ne $Task.UsingVariables) { - foreach ($usingVar in $Task.UsingVariables) { - $parameters[$usingVar.NewName] = $usingVar.Value - } - } - - $name = New-PodeGuid - $result = [System.Management.Automation.PSDataCollection[psobject]]::new() - $runspace = Add-PodeRunspace -Type Tasks -ScriptBlock (($Task.Script).GetNewClosure()) -Parameters $parameters -OutputStream $result -PassThru + # what is the expire time if using "create" timeout? + $expireTime = [datetime]::MaxValue + $createTime = [datetime]::UtcNow - if ($Timeout -ge 0) { - $expireTime = [datetime]::UtcNow.AddSeconds($Timeout) - } - else { - $expireTime = [datetime]::MaxValue + if (($TimeoutFrom -ieq 'Create') -and ($Timeout -ge 0)) { + $expireTime = $createTime.AddSeconds($Timeout) } - $PodeContext.Tasks.Results[$name] = @{ - ID = $name + # add task process + $result = [System.Management.Automation.PSDataCollection[psobject]]::new() + $PodeContext.Tasks.Processes[$processId] = @{ + ID = $processId Task = $Task.Name - Runspace = $runspace + Runspace = $null Result = $result + CreateTime = $createTime + StartTime = $null CompletedTime = $null ExpireTime = $expireTime - Timeout = $Timeout + Timeout = @{ + Value = $Timeout + From = $TimeoutFrom + } + State = 'Pending' } - return $PodeContext.Tasks.Results[$name] + # start the task runspace + $scriptblock = Get-PodeTaskScriptBlock + $runspace = Add-PodeRunspace -Type Tasks -Name $Task.Name -ScriptBlock $scriptblock -Parameters $parameters -OutputStream $result -PassThru + + # add runspace to process + $PodeContext.Tasks.Processes[$processId].Runspace = $runspace + + # return the task process + return $PodeContext.Tasks.Processes[$processId] } catch { $_ | Write-PodeErrorLog } } +function Get-PodeTaskScriptBlock { + return { + param($ProcessId, $ArgumentList) + + try { + $process = $PodeContext.Tasks.Processes[$ProcessId] + if ($null -eq $process) { + # Task process does not exist: $ProcessId + throw ($PodeLocale.taskProcessDoesNotExistExceptionMessage -f $ProcessId) + } + + # set the start time and state + $process.StartTime = [datetime]::UtcNow + $process.State = 'Running' + + # set the expire time of timeout based on "start" time + if (($process.Timeout.From -ieq 'Start') -and ($process.Timeout.Value -ge 0)) { + $process.ExpireTime = $process.StartTime.AddSeconds($process.Timeout.Value) + } + + # get the task, error if not found + $task = $PodeContext.Tasks.Items[$process.Task] + if ($null -eq $task) { + # Task does not exist + throw ($PodeLocale.taskDoesNotExistExceptionMessage -f $process.Task) + } + + # build the script arguments + $TaskEvent = @{ + Lockable = $PodeContext.Threading.Lockables.Global + Sender = $task + Timestamp = [DateTime]::UtcNow + Metadata = @{} + } + + $_args = @{ Event = $TaskEvent } + + if ($null -ne $task.Arguments) { + foreach ($key in $task.Arguments.Keys) { + $_args[$key] = $task.Arguments[$key] + } + } + + if ($null -ne $ArgumentList) { + foreach ($key in $ArgumentList.Keys) { + $_args[$key] = $ArgumentList[$key] + } + } + + # add any using variables + if ($null -ne $task.UsingVariables) { + foreach ($usingVar in $task.UsingVariables) { + $_args[$usingVar.NewName] = $usingVar.Value + } + } + + # invoke the script from the task + Invoke-PodeScriptBlock -ScriptBlock $task.Script -Arguments $_args -Scoped -Splat -Return + + # set the state to completed + $process.State = 'Completed' + } + catch { + # update the state + if ($null -ne $process) { + $process.State = 'Failed' + } + + # log the error + $_ | Write-PodeErrorLog + } + finally { + Invoke-PodeGC + } + } +} + function Wait-PodeNetTaskInternal { [CmdletBinding()] [OutputType([object])] @@ -168,7 +253,8 @@ function Wait-PodeNetTaskInternal { # if the main task isnt complete, it timed out if (($null -ne $timeoutTask) -and (!$Task.IsCompleted)) { - throw [System.TimeoutException]::new("Task has timed out after $($Timeout)ms") + # "Task has timed out after $($Timeout)ms") + throw [System.TimeoutException]::new($PodeLocale.taskTimedOutExceptionMessage -f $Timeout) } # only return a value if the result has one diff --git a/src/Private/TcpServer.ps1 b/src/Private/TcpServer.ps1 index 60156d15b..43a3c38c5 100644 --- a/src/Private/TcpServer.ps1 +++ b/src/Private/TcpServer.ps1 @@ -4,7 +4,7 @@ function Start-PodeTcpServer { # work out which endpoints to listen on $endpoints = @() - @(Get-PodeEndpoints -Type Tcp) | ForEach-Object { + @(Get-PodeEndpointByProtocolType -Type Tcp) | ForEach-Object { # get the ip address $_ip = [string]($_.Address) $_ip = Get-PodeIPAddressesForHostname -Hostname $_ip -Type All | Select-Object -First 1 @@ -44,7 +44,7 @@ function Start-PodeTcpServer { # create the listener $listener = [PodeListener]::new($PodeContext.Tokens.Cancellation.Token) $listener.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled) - $listener.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevels) + $listener.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevel) $listener.RequestTimeout = $PodeContext.Server.Request.Timeout $listener.RequestBodySize = $PodeContext.Server.Request.BodySize @@ -169,7 +169,9 @@ function Start-PodeTcpServer { $Request.UpgradeToSSL() } } - catch [System.OperationCanceledException] {} + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } catch { $_ | Write-PodeErrorLog $_.Exception | Write-PodeErrorLog -CheckInnerException @@ -181,7 +183,9 @@ function Start-PodeTcpServer { } } } - catch [System.OperationCanceledException] {} + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } catch { $_ | Write-PodeErrorLog $_.Exception | Write-PodeErrorLog -CheckInnerException @@ -191,7 +195,7 @@ function Start-PodeTcpServer { # start the runspace for listening on x-number of threads 1..$PodeContext.Threads.General | ForEach-Object { - Add-PodeRunspace -Type Tcp -ScriptBlock $listenScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ } + Add-PodeRunspace -Type Tcp -Name 'Listener' -Id $_ -ScriptBlock $listenScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ } } # script to keep tcp server listening until cancelled @@ -207,7 +211,9 @@ function Start-PodeTcpServer { Start-Sleep -Seconds 1 } } - catch [System.OperationCanceledException] {} + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } catch { $_ | Write-PodeErrorLog $_.Exception | Write-PodeErrorLog -CheckInnerException @@ -218,7 +224,7 @@ function Start-PodeTcpServer { } } - Add-PodeRunspace -Type Tcp -ScriptBlock $waitScript -Parameters @{ 'Listener' = $listener } -NoProfile + Add-PodeRunspace -Type Tcp -Name 'KeepAlive' -ScriptBlock $waitScript -Parameters @{ 'Listener' = $listener } -NoProfile # state where we're running return @(foreach ($endpoint in $endpoints) { diff --git a/src/Private/Timers.ps1 b/src/Private/Timers.ps1 index ecca314ea..48b19ba9a 100644 --- a/src/Private/Timers.ps1 +++ b/src/Private/Timers.ps1 @@ -19,49 +19,72 @@ function Start-PodeTimerRunspace { } $script = { - while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { - $_now = [DateTime]::Now - - # only run timers that haven't completed, and have a next trigger in the past - $PodeContext.Timers.Items.Values | Where-Object { - !$_.Completed -and ($_.OnStart -or ($_.NextTriggerTime -le $_now)) - } | ForEach-Object { - $_.OnStart = $false - $_.Count++ - - # set last trigger to current next trigger - if ($null -ne $_.NextTriggerTime) { - $_.LastTriggerTime = $_.NextTriggerTime + try { + + while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + try { + $_now = [DateTime]::Now + + # only run timers that haven't completed, and have a next trigger in the past + foreach ($timer in $PodeContext.Timers.Items.Values) { + if ($timer.Completed -or (!$timer.OnStart -and ($timer.NextTriggerTime -gt $_now))) { + continue + } + + try { + $timer.OnStart = $false + $timer.Count++ + + # set last trigger to current next trigger + if ($null -ne $timer.NextTriggerTime) { + $timer.LastTriggerTime = $timer.NextTriggerTime + } + else { + $timer.LastTriggerTime = $_now + } + + # has the timer completed? + if (($timer.Limit -gt 0) -and ($timer.Count -ge $timer.Limit)) { + $timer.Completed = $true + } + + # next trigger + if (!$timer.Completed) { + $timer.NextTriggerTime = $_now.AddSeconds($timer.Interval) + } + else { + $timer.NextTriggerTime = $null + } + + # run the timer + Invoke-PodeInternalTimer -Timer $timer + } + catch { + $_ | Write-PodeErrorLog + } + } + + Start-Sleep -Seconds 1 } - else { - $_.LastTriggerTime = [datetime]::Now + catch { + $_ | Write-PodeErrorLog } - - # has the timer completed? - if (($_.Limit -gt 0) -and ($_.Count -ge $_.Limit)) { - $_.Completed = $true - } - - # next trigger - if (!$_.Completed) { - $_.NextTriggerTime = $_now.AddSeconds($_.Interval) - } - else { - $_.NextTriggerTime = $null - } - - # run the timer - Invoke-PodeInternalTimer -Timer $_ } - - Start-Sleep -Seconds 1 + } + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } + catch { + $_ | Write-PodeErrorLog + throw $_.Exception } } - Add-PodeRunspace -Type Main -ScriptBlock $script + Add-PodeRunspace -Type Timers -Name 'Scheduler' -ScriptBlock $script } function Invoke-PodeInternalTimer { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] param( [Parameter(Mandatory = $true)] $Timer, @@ -72,10 +95,11 @@ function Invoke-PodeInternalTimer { ) try { - $global:TimerEvent = @{ - Lockable = $PodeContext.Threading.Lockables.Global - Sender = $Timer - Metadata = @{} + $TimerEvent = @{ + Lockable = $PodeContext.Threading.Lockables.Global + Sender = $Timer + Timestamp = [DateTime]::UtcNow + Metadata = @{} } # add main timer args @@ -90,9 +114,12 @@ function Invoke-PodeInternalTimer { } # invoke timer - $null = Invoke-PodeScriptBlock -ScriptBlock $Timer.Script -Arguments $_args -UsingVariables $Timer.UsingVariables -Scoped -Splat + Invoke-PodeScriptBlock -ScriptBlock $Timer.Script.GetNewClosure() -Arguments $_args -UsingVariables $Timer.UsingVariables -Scoped -Splat -NoNewClosure } catch { $_ | Write-PodeErrorLog } + finally { + Invoke-PodeGC + } } \ No newline at end of file diff --git a/src/Private/Verbs.ps1 b/src/Private/Verbs.ps1 index 002e61571..67095e0ee 100644 --- a/src/Private/Verbs.ps1 +++ b/src/Private/Verbs.ps1 @@ -117,9 +117,9 @@ function Test-PodeVerbAndError { } if ([string]::IsNullOrEmpty($_url)) { - throw "[Verb] $($Verb): Already defined" + throw ($PodeLocale.verbAlreadyDefinedExceptionMessage -f $Verb) #"[Verb] $($Verb): Already defined" } else { - throw "[Verb] $($Verb): Already defined for $($_url)" + throw ($PodeLocale.verbAlreadyDefinedForUrlExceptionMessage -f $Verb, $_url) # "[Verb] $($Verb): Already defined for $($_url)" } } \ No newline at end of file diff --git a/src/Private/WebSockets.ps1 b/src/Private/WebSockets.ps1 index f72aef50a..b1c0cdc8f 100644 --- a/src/Private/WebSockets.ps1 +++ b/src/Private/WebSockets.ps1 @@ -22,7 +22,7 @@ function New-PodeWebSocketReceiver { try { $receiver = [PodeReceiver]::new($PodeContext.Tokens.Cancellation.Token) $receiver.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled) - $receiver.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevels) + $receiver.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevel) $PodeContext.Server.WebSockets.Receiver = $receiver $PodeContext.Receivers += $receiver } @@ -39,7 +39,7 @@ function Start-PodeWebSocketRunspace { return } - # script for listening out of for incoming requests + # script for listening out of for incoming requests (Receiver) $receiveScript = { param( [Parameter(Mandatory = $true)] @@ -81,7 +81,9 @@ function Start-PodeWebSocketRunspace { # invoke websocket script $null = Invoke-PodeScriptBlock -ScriptBlock $websocket.Logic -Arguments $websocket.Arguments -UsingVariables $websocket.UsingVariables -Scoped -Splat } - catch [System.OperationCanceledException] {} + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } catch { $_ | Write-PodeErrorLog $_.Exception | Write-PodeErrorLog -CheckInnerException @@ -93,7 +95,9 @@ function Start-PodeWebSocketRunspace { } } } - catch [System.OperationCanceledException] {} + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } catch { $_ | Write-PodeErrorLog $_.Exception | Write-PodeErrorLog -CheckInnerException @@ -103,7 +107,7 @@ function Start-PodeWebSocketRunspace { # start the runspace for listening on x-number of threads 1..$PodeContext.Threads.WebSockets | ForEach-Object { - Add-PodeRunspace -Type WebSockets -ScriptBlock $receiveScript -Parameters @{ 'Receiver' = $PodeContext.Server.WebSockets.Receiver; 'ThreadId' = $_ } + Add-PodeRunspace -Type WebSockets -Name 'Receiver' -Id $_ -ScriptBlock $receiveScript -Parameters @{ 'Receiver' = $PodeContext.Server.WebSockets.Receiver; 'ThreadId' = $_ } } # script to keep websocket server receiving until cancelled @@ -119,7 +123,9 @@ function Start-PodeWebSocketRunspace { Start-Sleep -Seconds 1 } } - catch [System.OperationCanceledException] {} + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } catch { $_ | Write-PodeErrorLog $_.Exception | Write-PodeErrorLog -CheckInnerException @@ -130,5 +136,5 @@ function Start-PodeWebSocketRunspace { } } - Add-PodeRunspace -Type WebSockets -ScriptBlock $waitScript -Parameters @{ 'Receiver' = $PodeContext.Server.WebSockets.Receiver } -NoProfile + Add-PodeRunspace -Type WebSockets -Name 'KeepAlive' -ScriptBlock $waitScript -Parameters @{ 'Receiver' = $PodeContext.Server.WebSockets.Receiver } -NoProfile } \ No newline at end of file diff --git a/src/Public/Access.ps1 b/src/Public/Access.ps1 index 3e7a56ef5..ebf1cb3b5 100644 --- a/src/Public/Access.ps1 +++ b/src/Public/Access.ps1 @@ -69,7 +69,8 @@ function New-PodeAccessScheme { # for custom access a validator is mandatory if ($Custom) { if ([string]::IsNullOrWhiteSpace($Path) -and (Test-PodeIsEmpty $ScriptBlock)) { - throw 'A Path or ScriptBlock is required for sourcing the Custom access values' + # A Path or ScriptBlock is required for sourcing the Custom access values + throw ($PodeLocale.customAccessPathOrScriptBlockRequiredExceptionMessage) } } @@ -171,33 +172,46 @@ function Add-PodeAccess { [string] $Match = 'One' ) + begin { + $pipelineItemCount = 0 + } - # check name unique - if (Test-PodeAccessExists -Name $Name) { - throw "Access method already defined: $($Name)" + process { + $pipelineItemCount++ } - # parse using variables in validator scriptblock - $scriptObj = $null - if (!(Test-PodeIsEmpty $ScriptBlock)) { - $ScriptBlock, $usingScriptVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState - $scriptObj = @{ - Script = $ScriptBlock - UsingVariables = $usingScriptVars + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + # check name unique + if (Test-PodeAccessExists -Name $Name) { + # Access method already defined: $($Name) + throw ($PodeLocale.accessMethodAlreadyDefinedExceptionMessage -f $Name) } - } - # add access object - $PodeContext.Server.Authorisations.Methods[$Name] = @{ - Name = $Name - Description = $Description - Scheme = $Scheme - ScriptBlock = $scriptObj - Arguments = $ArgumentList - Match = $Match.ToLowerInvariant() - Cache = @{} - Merged = $false - Parent = $null + # parse using variables in validator scriptblock + $scriptObj = $null + if (!(Test-PodeIsEmpty $ScriptBlock)) { + $ScriptBlock, $usingScriptVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + $scriptObj = @{ + Script = $ScriptBlock + UsingVariables = $usingScriptVars + } + } + + # add access object + $PodeContext.Server.Authorisations.Methods[$Name] = @{ + Name = $Name + Description = $Description + Scheme = $Scheme + ScriptBlock = $scriptObj + Arguments = $ArgumentList + Match = $Match.ToLowerInvariant() + Cache = @{} + Merged = $false + Parent = $null + } } } @@ -241,13 +255,13 @@ function Merge-PodeAccess { # ensure the name doesn't already exist if (Test-PodeAccessExists -Name $Name) { - throw "Access method already defined: $($Name)" + throw ($PodeLocale.accessMethodAlreadyDefinedExceptionMessage -f $Name) #"Access method already defined: $($Name)" } # ensure all the access methods exist foreach ($accName in $Access) { if (!(Test-PodeAccessExists -Name $accName)) { - throw "Access method does not exist for merging: $($accName)" + throw ($PodeLocale.accessMethodNotExistForMergingExceptionMessage -f $accName) #"Access method does not exist for merging: $($accName)" } } @@ -313,7 +327,7 @@ function Add-PodeAccessCustom { end { foreach ($r in $routes) { if ($r.AccessMeta.Custom.ContainsKey($Name)) { - throw "Route '[$($r.Method)] $($r.Path)' already contains Custom Access with name '$($Name)'" + throw ($PodeLocale.routeAlreadyContainsCustomAccessExceptionMessage -f $r.Method, $r.Path, $Name) #"Route '[$($r.Method)] $($r.Path)' already contains Custom Access with name '$($Name)'" } $r.AccessMeta.Custom[$Name] = $Value @@ -374,13 +388,13 @@ The Name of the Access method. if (Test-PodeAccessExists -Name 'Example') { } #> function Test-PodeAccessExists { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $Name ) - return $PodeContext.Server.Authorisations.Methods.ContainsKey($Name) } @@ -603,8 +617,9 @@ function Remove-PodeAccess { [string] $Name ) - - $null = $PodeContext.Server.Authorisations.Methods.Remove($Name) + process { + $null = $PodeContext.Server.Authorisations.Methods.Remove($Name) + } } <# @@ -663,7 +678,7 @@ function Add-PodeAccessMiddleware { ) if (!(Test-PodeAccessExists -Name $Access)) { - throw "Access method does not exist: $($Access)" + throw ($PodeLocale.accessMethodNotExistExceptionMessage -f $Access) #"Access method does not exist: $($Access)" } Get-PodeAccessMiddlewareScript | diff --git a/src/Public/Authentication.ps1 b/src/Public/Authentication.ps1 index 8fbbd5e5c..64d6ea2df 100644 --- a/src/Public/Authentication.ps1 +++ b/src/Public/Authentication.ps1 @@ -159,7 +159,8 @@ function New-PodeAuthScheme { [Parameter(Mandatory = $true, ParameterSetName = 'Custom')] [ValidateScript({ if (Test-PodeIsEmpty $_) { - throw 'A non-empty ScriptBlock is required for the Custom authentication scheme' + # A non-empty ScriptBlock is required for the Custom authentication scheme + throw ($PodeLocale.nonEmptyScriptBlockRequiredForCustomAuthExceptionMessage) } return $true @@ -286,237 +287,253 @@ function New-PodeAuthScheme { [string] $Secret ) + begin { + $pipelineItemCount = 0 + } - # default realm - $_realm = 'User' - - # convert any middleware into valid hashtables - $Middleware = @(ConvertTo-PodeMiddleware -Middleware $Middleware -PSSession $PSCmdlet.SessionState) + process { + $pipelineItemCount++ + } - # configure the auth scheme - switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { - 'basic' { - return @{ - Name = (Protect-PodeValue -Value $HeaderTag -Default 'Basic') - Realm = (Protect-PodeValue -Value $Realm -Default $_realm) - ScriptBlock = @{ - Script = (Get-PodeAuthBasicType) - UsingVariables = $null - } - PostValidator = $null - Middleware = $Middleware - InnerScheme = $InnerScheme - Scheme = 'http' - Arguments = @{ - Description = $Description - HeaderTag = (Protect-PodeValue -Value $HeaderTag -Default 'Basic') - Encoding = (Protect-PodeValue -Value $Encoding -Default 'ISO-8859-1') - AsCredential = $AsCredential - } - } + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) } - - 'clientcertificate' { - return @{ - Name = 'Mutual' - Realm = (Protect-PodeValue -Value $Realm -Default $_realm) - ScriptBlock = @{ - Script = (Get-PodeAuthClientCertificateType) - UsingVariables = $null + # default realm + $_realm = 'User' + + # convert any middleware into valid hashtables + $Middleware = @(ConvertTo-PodeMiddleware -Middleware $Middleware -PSSession $PSCmdlet.SessionState) + + # configure the auth scheme + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + 'basic' { + return @{ + Name = (Protect-PodeValue -Value $HeaderTag -Default 'Basic') + Realm = (Protect-PodeValue -Value $Realm -Default $_realm) + ScriptBlock = @{ + Script = (Get-PodeAuthBasicType) + UsingVariables = $null + } + PostValidator = $null + Middleware = $Middleware + InnerScheme = $InnerScheme + Scheme = 'http' + Arguments = @{ + Description = $Description + HeaderTag = (Protect-PodeValue -Value $HeaderTag -Default 'Basic') + Encoding = (Protect-PodeValue -Value $Encoding -Default 'ISO-8859-1') + AsCredential = $AsCredential + } } - PostValidator = $null - Middleware = $Middleware - InnerScheme = $InnerScheme - Scheme = 'http' - Arguments = @{} } - } - 'digest' { - return @{ - Name = 'Digest' - Realm = (Protect-PodeValue -Value $Realm -Default $_realm) - ScriptBlock = @{ - Script = (Get-PodeAuthDigestType) - UsingVariables = $null - } - PostValidator = @{ - Script = (Get-PodeAuthDigestPostValidator) - UsingVariables = $null - } - Middleware = $Middleware - InnerScheme = $InnerScheme - Scheme = 'http' - Arguments = @{ - HeaderTag = (Protect-PodeValue -Value $HeaderTag -Default 'Digest') + 'clientcertificate' { + return @{ + Name = 'Mutual' + Realm = (Protect-PodeValue -Value $Realm -Default $_realm) + ScriptBlock = @{ + Script = (Get-PodeAuthClientCertificateType) + UsingVariables = $null + } + PostValidator = $null + Middleware = $Middleware + InnerScheme = $InnerScheme + Scheme = 'http' + Arguments = @{} } } - } - 'bearer' { - $secretBytes = $null - if (![string]::IsNullOrWhiteSpace($Secret)) { - $secretBytes = [System.Text.Encoding]::UTF8.GetBytes($Secret) + 'digest' { + return @{ + Name = 'Digest' + Realm = (Protect-PodeValue -Value $Realm -Default $_realm) + ScriptBlock = @{ + Script = (Get-PodeAuthDigestType) + UsingVariables = $null + } + PostValidator = @{ + Script = (Get-PodeAuthDigestPostValidator) + UsingVariables = $null + } + Middleware = $Middleware + InnerScheme = $InnerScheme + Scheme = 'http' + Arguments = @{ + HeaderTag = (Protect-PodeValue -Value $HeaderTag -Default 'Digest') + } + } } - return @{ - Name = 'Bearer' - Realm = (Protect-PodeValue -Value $Realm -Default $_realm) - ScriptBlock = @{ - Script = (Get-PodeAuthBearerType) - UsingVariables = $null + 'bearer' { + $secretBytes = $null + if (![string]::IsNullOrWhiteSpace($Secret)) { + $secretBytes = [System.Text.Encoding]::UTF8.GetBytes($Secret) } - PostValidator = @{ - Script = (Get-PodeAuthBearerPostValidator) - UsingVariables = $null - } - Middleware = $Middleware - Scheme = 'http' - InnerScheme = $InnerScheme - Arguments = @{ - Description = $Description - HeaderTag = (Protect-PodeValue -Value $HeaderTag -Default 'Bearer') - Scopes = $Scope - AsJWT = $AsJWT - Secret = $secretBytes + + return @{ + Name = 'Bearer' + Realm = (Protect-PodeValue -Value $Realm -Default $_realm) + ScriptBlock = @{ + Script = (Get-PodeAuthBearerType) + UsingVariables = $null + } + PostValidator = @{ + Script = (Get-PodeAuthBearerPostValidator) + UsingVariables = $null + } + Middleware = $Middleware + Scheme = 'http' + InnerScheme = $InnerScheme + Arguments = @{ + Description = $Description + HeaderTag = (Protect-PodeValue -Value $HeaderTag -Default 'Bearer') + Scopes = $Scope + AsJWT = $AsJWT + Secret = $secretBytes + } } } - } - 'form' { - return @{ - Name = 'Form' - Realm = (Protect-PodeValue -Value $Realm -Default $_realm) - ScriptBlock = @{ - Script = (Get-PodeAuthFormType) - UsingVariables = $null - } - PostValidator = $null - Middleware = $Middleware - InnerScheme = $InnerScheme - Scheme = 'http' - Arguments = @{ - Description = $Description - Fields = @{ - Username = (Protect-PodeValue -Value $UsernameField -Default 'username') - Password = (Protect-PodeValue -Value $PasswordField -Default 'password') + 'form' { + return @{ + Name = 'Form' + Realm = (Protect-PodeValue -Value $Realm -Default $_realm) + ScriptBlock = @{ + Script = (Get-PodeAuthFormType) + UsingVariables = $null + } + PostValidator = $null + Middleware = $Middleware + InnerScheme = $InnerScheme + Scheme = 'http' + Arguments = @{ + Description = $Description + Fields = @{ + Username = (Protect-PodeValue -Value $UsernameField -Default 'username') + Password = (Protect-PodeValue -Value $PasswordField -Default 'password') + } + AsCredential = $AsCredential } - AsCredential = $AsCredential } } - } - 'oauth2' { - if (($null -ne $InnerScheme) -and ($InnerScheme.Name -inotin @('basic', 'form'))) { - throw "OAuth2 InnerScheme can only be one of either Basic or Form authentication, but got: $($InnerScheme.Name)" - } + 'oauth2' { + if (($null -ne $InnerScheme) -and ($InnerScheme.Name -inotin @('basic', 'form'))) { + # OAuth2 InnerScheme can only be one of either Basic or Form authentication, but got: {0} + throw ($PodeLocale.oauth2InnerSchemeInvalidExceptionMessage -f $InnerScheme.Name) + } - if (($null -eq $InnerScheme) -and [string]::IsNullOrWhiteSpace($AuthoriseUrl)) { - throw 'OAuth2 requires an Authorise URL to be supplied' - } + if (($null -eq $InnerScheme) -and [string]::IsNullOrWhiteSpace($AuthoriseUrl)) { + # OAuth2 requires an Authorise URL to be supplied + throw ($PodeLocale.oauth2RequiresAuthorizeUrlExceptionMessage) + } - if ($UsePKCE -and !(Test-PodeSessionsEnabled)) { - throw 'Sessions are required to use OAuth2 with PKCE' - } + if ($UsePKCE -and !(Test-PodeSessionsEnabled)) { + # Sessions are required to use OAuth2 with PKCE + throw ($PodeLocale.sessionsRequiredForOAuth2WithPKCEExceptionMessage) + } - if (!$UsePKCE -and [string]::IsNullOrEmpty($ClientSecret)) { - throw 'OAuth2 requires a Client Secret when not using PKCE' - } - return @{ - Name = 'OAuth2' - Realm = (Protect-PodeValue -Value $Realm -Default $_realm) - ScriptBlock = @{ - Script = (Get-PodeAuthOAuth2Type) - UsingVariables = $null + if (!$UsePKCE -and [string]::IsNullOrEmpty($ClientSecret)) { + # OAuth2 requires a Client Secret when not using PKCE + throw ($PodeLocale.oauth2ClientSecretRequiredExceptionMessage) } - PostValidator = $null - Middleware = $Middleware - Scheme = 'oauth2' - InnerScheme = $InnerScheme - Arguments = @{ - Description = $Description - Scopes = $Scope - PKCE = @{ - Enabled = $UsePKCE - CodeChallenge = @{ - Method = $CodeChallengeMethod - } - } - Client = @{ - ID = $ClientId - Secret = $ClientSecret + return @{ + Name = 'OAuth2' + Realm = (Protect-PodeValue -Value $Realm -Default $_realm) + ScriptBlock = @{ + Script = (Get-PodeAuthOAuth2Type) + UsingVariables = $null } - Urls = @{ - Redirect = $RedirectUrl - Authorise = $AuthoriseUrl - Token = $TokenUrl - User = @{ - Url = $UserUrl - Method = (Protect-PodeValue -Value $UserUrlMethod -Default 'Post') + PostValidator = $null + Middleware = $Middleware + Scheme = 'oauth2' + InnerScheme = $InnerScheme + Arguments = @{ + Description = $Description + Scopes = $Scope + PKCE = @{ + Enabled = $UsePKCE + CodeChallenge = @{ + Method = $CodeChallengeMethod + } + } + Client = @{ + ID = $ClientId + Secret = $ClientSecret + } + Urls = @{ + Redirect = $RedirectUrl + Authorise = $AuthoriseUrl + Token = $TokenUrl + User = @{ + Url = $UserUrl + Method = (Protect-PodeValue -Value $UserUrlMethod -Default 'Post') + } } } } } - } - 'apikey' { - # set default location name - if ([string]::IsNullOrWhiteSpace($LocationName)) { - $LocationName = (@{ - Header = 'X-API-KEY' - Query = 'api_key' - Cookie = 'X-API-KEY' - })[$Location] - } - - $secretBytes = $null - if (![string]::IsNullOrWhiteSpace($Secret)) { - $secretBytes = [System.Text.Encoding]::UTF8.GetBytes($Secret) - } + 'apikey' { + # set default location name + if ([string]::IsNullOrWhiteSpace($LocationName)) { + $LocationName = (@{ + Header = 'X-API-KEY' + Query = 'api_key' + Cookie = 'X-API-KEY' + })[$Location] + } - return @{ - Name = 'ApiKey' - Realm = (Protect-PodeValue -Value $Realm -Default $_realm) - ScriptBlock = @{ - Script = (Get-PodeAuthApiKeyType) - UsingVariables = $null + $secretBytes = $null + if (![string]::IsNullOrWhiteSpace($Secret)) { + $secretBytes = [System.Text.Encoding]::UTF8.GetBytes($Secret) } - PostValidator = $null - Middleware = $Middleware - InnerScheme = $InnerScheme - Scheme = 'apiKey' - Arguments = @{ - Description = $Description - Location = $Location - LocationName = $LocationName - AsJWT = $AsJWT - Secret = $secretBytes + + return @{ + Name = 'ApiKey' + Realm = (Protect-PodeValue -Value $Realm -Default $_realm) + ScriptBlock = @{ + Script = (Get-PodeAuthApiKeyType) + UsingVariables = $null + } + PostValidator = $null + Middleware = $Middleware + InnerScheme = $InnerScheme + Scheme = 'apiKey' + Arguments = @{ + Description = $Description + Location = $Location + LocationName = $LocationName + AsJWT = $AsJWT + Secret = $secretBytes + } } } - } - - 'custom' { - $ScriptBlock, $usingScriptVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState - if ($null -ne $PostValidator) { - $PostValidator, $usingPostVars = Convert-PodeScopedVariables -ScriptBlock $PostValidator -PSSession $PSCmdlet.SessionState - } + 'custom' { + $ScriptBlock, $usingScriptVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState - return @{ - Name = $Name - Realm = (Protect-PodeValue -Value $Realm -Default $_realm) - InnerScheme = $InnerScheme - Scheme = $Type.ToLowerInvariant() - ScriptBlock = @{ - Script = $ScriptBlock - UsingVariables = $usingScriptVars + if ($null -ne $PostValidator) { + $PostValidator, $usingPostVars = Convert-PodeScopedVariables -ScriptBlock $PostValidator -PSSession $PSCmdlet.SessionState } - PostValidator = @{ - Script = $PostValidator - UsingVariables = $usingPostVars + + return @{ + Name = $Name + Realm = (Protect-PodeValue -Value $Realm -Default $_realm) + InnerScheme = $InnerScheme + Scheme = $Type.ToLowerInvariant() + ScriptBlock = @{ + Script = $ScriptBlock + UsingVariables = $usingScriptVars + } + PostValidator = @{ + Script = $PostValidator + UsingVariables = $usingPostVars + } + Middleware = $Middleware + Arguments = $ArgumentList } - Middleware = $Middleware - Arguments = $ArgumentList } } } @@ -558,6 +575,7 @@ New-PodeAuthAzureADScheme -Tenant 123-456-678 -ClientId some_id -UsePKCE #> function New-PodeAuthAzureADScheme { [CmdletBinding()] + [OutputType([hashtable])] param( [Parameter()] [ValidateNotNullOrEmpty()] @@ -587,18 +605,31 @@ function New-PodeAuthAzureADScheme { [switch] $UsePKCE ) + begin { + $pipelineItemCount = 0 + } - return New-PodeAuthScheme ` - -OAuth2 ` - -ClientId $ClientId ` - -ClientSecret $ClientSecret ` - -AuthoriseUrl "https://login.microsoftonline.com/$($Tenant)/oauth2/v2.0/authorize" ` - -TokenUrl "https://login.microsoftonline.com/$($Tenant)/oauth2/v2.0/token" ` - -UserUrl 'https://graph.microsoft.com/oidc/userinfo' ` - -RedirectUrl $RedirectUrl ` - -InnerScheme $InnerScheme ` - -Middleware $Middleware ` - -UsePKCE:$UsePKCE + process { + + $pipelineItemCount++ + } + + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + return New-PodeAuthScheme ` + -OAuth2 ` + -ClientId $ClientId ` + -ClientSecret $ClientSecret ` + -AuthoriseUrl "https://login.microsoftonline.com/$($Tenant)/oauth2/v2.0/authorize" ` + -TokenUrl "https://login.microsoftonline.com/$($Tenant)/oauth2/v2.0/token" ` + -UserUrl 'https://graph.microsoft.com/oidc/userinfo' ` + -RedirectUrl $RedirectUrl ` + -InnerScheme $InnerScheme ` + -Middleware $Middleware ` + -UsePKCE:$UsePKCE + } } <# @@ -631,6 +662,7 @@ New-PodeAuthTwitterScheme -ClientId some_id -UsePKCE #> function New-PodeAuthTwitterScheme { [CmdletBinding()] + [OutputType([hashtable])] param( [Parameter(Mandatory = $true)] [string] @@ -717,7 +749,8 @@ function Add-PodeAuth { [Parameter(Mandatory = $true)] [ValidateScript({ if (Test-PodeIsEmpty $_) { - throw 'A non-empty ScriptBlock is required for the authentication method' + # A non-empty ScriptBlock is required for the authentication method + throw ($PodeLocale.nonEmptyScriptBlockRequiredForAuthMethodExceptionMessage) } return $true @@ -747,51 +780,67 @@ function Add-PodeAuth { [switch] $SuccessUseOrigin ) - - # ensure the name doesn't already exist - if (Test-PodeAuthExists -Name $Name) { - throw "Authentication method already defined: $($Name)" + begin { + $pipelineItemCount = 0 } - # ensure the Scheme contains a scriptblock - if (Test-PodeIsEmpty $Scheme.ScriptBlock) { - throw "The supplied '$($Scheme.Name)' Scheme for the '$($Name)' authentication validator requires a valid ScriptBlock" - } + process { - # if we're using sessions, ensure sessions have been setup - if (!$Sessionless -and !(Test-PodeSessionsEnabled)) { - throw 'Sessions are required to use session persistent authentication' + $pipelineItemCount++ } - # check for scoped vars - $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + # ensure the name doesn't already exist + if (Test-PodeAuthExists -Name $Name) { + # Authentication method already defined: {0} + throw ($PodeLocale.authMethodAlreadyDefinedExceptionMessage -f $Name) + } - # add auth method to server - $PodeContext.Server.Authentications.Methods[$Name] = @{ - Name = $Name - Scheme = $Scheme - ScriptBlock = $ScriptBlock - UsingVariables = $usingVars - Arguments = $ArgumentList - Sessionless = $Sessionless.IsPresent - Failure = @{ - Url = $FailureUrl - Message = $FailureMessage + # ensure the Scheme contains a scriptblock + if (Test-PodeIsEmpty $Scheme.ScriptBlock) { + # The supplied scheme for the '{0}' authentication validator requires a valid ScriptBlock + throw ($PodeLocale.schemeRequiresValidScriptBlockExceptionMessage -f $Name) } - Success = @{ - Url = $SuccessUrl - UseOrigin = $SuccessUseOrigin.IsPresent + + # if we're using sessions, ensure sessions have been setup + if (!$Sessionless -and !(Test-PodeSessionsEnabled)) { + # Sessions are required to use session persistent authentication + throw ($PodeLocale.sessionsRequiredForSessionPersistentAuthExceptionMessage) + } + + # check for scoped vars + $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + + # add auth method to server + $PodeContext.Server.Authentications.Methods[$Name] = @{ + Name = $Name + Scheme = $Scheme + ScriptBlock = $ScriptBlock + UsingVariables = $usingVars + Arguments = $ArgumentList + Sessionless = $Sessionless.IsPresent + Failure = @{ + Url = $FailureUrl + Message = $FailureMessage + } + Success = @{ + Url = $SuccessUrl + UseOrigin = $SuccessUseOrigin.IsPresent + } + Cache = @{} + Merged = $false + Parent = $null } - Cache = @{} - Merged = $false - Parent = $null - } - # if the scheme is oauth2, and there's no redirect, set up a default one - if (($Scheme.Name -ieq 'oauth2') -and ($null -eq $Scheme.InnerScheme) -and [string]::IsNullOrWhiteSpace($Scheme.Arguments.Urls.Redirect)) { - $path = '/oauth2/callback' - $Scheme.Arguments.Urls.Redirect = $path - Add-PodeRoute -Method Get -Path $path -Authentication $Name + # if the scheme is oauth2, and there's no redirect, set up a default one + if (($Scheme.Name -ieq 'oauth2') -and ($null -eq $Scheme.InnerScheme) -and [string]::IsNullOrWhiteSpace($Scheme.Arguments.Urls.Redirect)) { + $path = '/oauth2/callback' + $Scheme.Arguments.Urls.Redirect = $path + Add-PodeRoute -Method Get -Path $path -Authentication $Name + } } } @@ -902,24 +951,25 @@ function Merge-PodeAuth { # ensure the name doesn't already exist if (Test-PodeAuthExists -Name $Name) { - throw "Authentication method already defined: $($Name)" + # Authentication method already defined: { 0 } + throw ($PodeLocale.authMethodAlreadyDefinedExceptionMessage -f $Name) } # ensure all the auth methods exist foreach ($authName in $Authentication) { if (!(Test-PodeAuthExists -Name $authName)) { - throw "Authentication method does not exist for merging: $($authName)" + throw ($PodeLocale.authMethodNotExistForMergingExceptionMessage -f $authName) #"Authentication method does not exist for merging: $($authName)" } } # ensure the merge default is in the auth list if (![string]::IsNullOrEmpty($MergeDefault) -and ($MergeDefault -inotin @($Authentication))) { - throw "the MergeDefault Authentication '$($MergeDefault)' is not in the Authentication list supplied" + throw ($PodeLocale.mergeDefaultAuthNotInListExceptionMessage -f $MergeDefault) # "the MergeDefault Authentication '$($MergeDefault)' is not in the Authentication list supplied" } # ensure the default is in the auth list if (![string]::IsNullOrEmpty($Default) -and ($Default -inotin @($Authentication))) { - throw "the Default Authentication '$($Default)' is not in the Authentication list supplied" + throw ($PodeLocale.defaultAuthNotInListExceptionMessage -f $Default) # "the Default Authentication '$($Default)' is not in the Authentication list supplied" } # set default @@ -937,7 +987,8 @@ function Merge-PodeAuth { # if we're using sessions, ensure sessions have been setup if (!$Sessionless -and !(Test-PodeSessionsEnabled)) { - throw 'Sessions are required to use session persistent authentication' + # Sessions are required to use session persistent authentication + throw ($PodeLocale.sessionsRequiredForSessionPersistentAuthExceptionMessage) } # check failure url from default @@ -963,7 +1014,8 @@ function Merge-PodeAuth { # deal with using vars in scriptblock if (($Valid -ieq 'all') -and [string]::IsNullOrEmpty($MergeDefault)) { if ($null -eq $ScriptBlock) { - throw 'A Scriptblock for merging multiple authenticated users into 1 object is required When Valid is All' + # A Scriptblock for merging multiple authenticated users into 1 object is required When Valid is All + throw ($PodeLocale.scriptBlockRequiredForMergingUsersExceptionMessage) } $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState @@ -1020,6 +1072,7 @@ Get-PodeAuth -Name 'Main' #> function Get-PodeAuth { [CmdletBinding()] + [OutputType([hashtable])] param( [Parameter(Mandatory = $true)] [string] @@ -1028,7 +1081,7 @@ function Get-PodeAuth { # ensure the name exists if (!(Test-PodeAuthExists -Name $Name)) { - throw "Authentication method not defined: $($Name)" + throw ($PodeLocale.authenticationMethodDoesNotExistExceptionMessage -f $Name) # "Authentication method not defined: $($Name)" } # get auth method @@ -1049,7 +1102,9 @@ The Name of the Authentication method. if (Test-PodeAuthExists -Name BasicAuth) { ... } #> function Test-PodeAuthExists { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] + [OutputType([bool])] param( [Parameter(Mandatory = $true)] [string] @@ -1081,6 +1136,7 @@ if (Test-PodeAuth -Name 'FormAuth' -IgnoreSession) { ... } #> function Test-PodeAuth { [CmdletBinding()] + [OutputType([boolean])] param( [Parameter(Mandatory = $true)] [string] @@ -1261,78 +1317,95 @@ function Add-PodeAuthWindowsAd { [switch] $KeepCredential ) - - # ensure the name doesn't already exist - if (Test-PodeAuthExists -Name $Name) { - throw "Windows AD Authentication method already defined: $($Name)" + begin { + $pipelineItemCount = 0 } - # ensure the Scheme contains a scriptblock - if (Test-PodeIsEmpty $Scheme.ScriptBlock) { - throw "The supplied Scheme for the '$($Name)' Windows AD authentication validator requires a valid ScriptBlock" - } + process { - # if we're using sessions, ensure sessions have been setup - if (!$Sessionless -and !(Test-PodeSessionsEnabled)) { - throw 'Sessions are required to use session persistent authentication' + $pipelineItemCount++ } - # if AD module set, ensure we're on windows and the module is available, then import/export it - if ($ADModule) { - Import-PodeAuthADModule - } + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + # ensure the name doesn't already exist + if (Test-PodeAuthExists -Name $Name) { + # Authentication method already defined: {0} + throw ($PodeLocale.authMethodAlreadyDefinedExceptionMessage -f $Name) + } - # set server name if not passed - if ([string]::IsNullOrWhiteSpace($Fqdn)) { - $Fqdn = Get-PodeAuthDomainName + # ensure the Scheme contains a scriptblock + if (Test-PodeIsEmpty $Scheme.ScriptBlock) { + # The supplied Scheme for the '$($Name)' Windows AD authentication validator requires a valid ScriptBlock + throw ($PodeLocale.schemeRequiresValidScriptBlockExceptionMessage -f $Name) + } - if ([string]::IsNullOrWhiteSpace($Fqdn)) { - throw 'No domain server name has been supplied for Windows AD authentication' + # if we're using sessions, ensure sessions have been setup + if (!$Sessionless -and !(Test-PodeSessionsEnabled)) { + # Sessions are required to use session persistent authentication + throw ($PodeLocale.sessionsRequiredForSessionPersistentAuthExceptionMessage) } - } - # set the domain if not passed - if ([string]::IsNullOrWhiteSpace($Domain)) { - $Domain = ($Fqdn -split '\.')[0] - } + # if AD module set, ensure we're on windows and the module is available, then import/export it + if ($ADModule) { + Import-PodeAuthADModule + } - # if we have a scriptblock, deal with using vars - if ($null -ne $ScriptBlock) { - $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState - } + # set server name if not passed + if ([string]::IsNullOrWhiteSpace($Fqdn)) { + $Fqdn = Get-PodeAuthDomainName - # add Windows AD auth method to server - $PodeContext.Server.Authentications.Methods[$Name] = @{ - Name = $Name - Scheme = $Scheme - ScriptBlock = (Get-PodeAuthWindowsADMethod) - Arguments = @{ - Server = $Fqdn - Domain = $Domain - SearchBase = $SearchBase - Users = $Users - Groups = $Groups - NoGroups = $NoGroups - DirectGroups = $DirectGroups - KeepCredential = $KeepCredential - Provider = (Get-PodeAuthADProvider -OpenLDAP:$OpenLDAP -ADModule:$ADModule) - ScriptBlock = @{ - Script = $ScriptBlock - UsingVariables = $usingVars + if ([string]::IsNullOrWhiteSpace($Fqdn)) { + # No domain server name has been supplied for Windows AD authentication + throw ($PodeLocale.noDomainServerNameForWindowsAdAuthExceptionMessage) } } - Sessionless = $Sessionless - Failure = @{ - Url = $FailureUrl - Message = $FailureMessage + + # set the domain if not passed + if ([string]::IsNullOrWhiteSpace($Domain)) { + $Domain = ($Fqdn -split '\.')[0] } - Success = @{ - Url = $SuccessUrl - UseOrigin = $SuccessUseOrigin + + # if we have a scriptblock, deal with using vars + if ($null -ne $ScriptBlock) { + $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + } + + # add Windows AD auth method to server + $PodeContext.Server.Authentications.Methods[$Name] = @{ + Name = $Name + Scheme = $Scheme + ScriptBlock = (Get-PodeAuthWindowsADMethod) + Arguments = @{ + Server = $Fqdn + Domain = $Domain + SearchBase = $SearchBase + Users = $Users + Groups = $Groups + NoGroups = $NoGroups + DirectGroups = $DirectGroups + KeepCredential = $KeepCredential + Provider = (Get-PodeAuthADProvider -OpenLDAP:$OpenLDAP -ADModule:$ADModule) + ScriptBlock = @{ + Script = $ScriptBlock + UsingVariables = $usingVars + } + } + Sessionless = $Sessionless + Failure = @{ + Url = $FailureUrl + Message = $FailureMessage + } + Success = @{ + Url = $SuccessUrl + UseOrigin = $SuccessUseOrigin + } + Cache = @{} + Merged = $false + Parent = $null } - Cache = @{} - Merged = $false - Parent = $null } } @@ -1400,12 +1473,14 @@ function Add-PodeAuthSession { # if sessions haven't been setup, error if (!(Test-PodeSessionsEnabled)) { - throw 'Sessions have not been configured' + # Sessions have not been configured + throw ($PodeLocale.sessionsNotConfiguredExceptionMessage) } # ensure the name doesn't already exist if (Test-PodeAuthExists -Name $Name) { - throw "Authentication method already defined: $($Name)" + # Authentication method already defined: { 0 } + throw ($PodeLocale.authMethodAlreadyDefinedExceptionMessage -f $Name) } # if we have a scriptblock, deal with using vars @@ -1488,8 +1563,9 @@ function Remove-PodeAuth { [string] $Name ) - - $null = $PodeContext.Server.Authentications.Methods.Remove($Name) + process { + $null = $PodeContext.Server.Authentications.Methods.Remove($Name) + } } <# @@ -1559,7 +1635,7 @@ function Add-PodeAuthMiddleware { $DefinitionTag = Test-PodeOADefinitionTag -Tag $OADefinitionTag if (!(Test-PodeAuthExists -Name $Authentication)) { - throw "Authentication method does not exist: $($Authentication)" + throw ($PodeLocale.authenticationMethodDoesNotExistExceptionMessage -f $Authentication) # "Authentication method does not exist: $($Authentication)" } Get-PodeAuthMiddlewareScript | @@ -1685,12 +1761,14 @@ function Add-PodeAuthIIS { # ensure we're on Windows! if (!(Test-PodeIsWindows)) { - throw 'IIS Authentication support is for Windows only' + # IIS Authentication support is for Windows only + throw ($PodeLocale.iisAuthSupportIsForWindowsOnlyExceptionMessage) } # ensure the name doesn't already exist if (Test-PodeAuthExists -Name $Name) { - throw "IIS Authentication method already defined: $($Name)" + # Authentication method already defined: {0} + throw ($PodeLocale.authMethodAlreadyDefinedExceptionMessage -f $Name) } # if AD module set, ensure we're on windows and the module is available, then import/export it @@ -1845,67 +1923,84 @@ function Add-PodeAuthUserFile { [switch] $SuccessUseOrigin ) - - # ensure the name doesn't already exist - if (Test-PodeAuthExists -Name $Name) { - throw "User File Authentication method already defined: $($Name)" + begin { + $pipelineItemCount = 0 } - # ensure the Scheme contains a scriptblock - if (Test-PodeIsEmpty $Scheme.ScriptBlock) { - throw "The supplied Scheme for the '$($Name)' User File authentication validator requires a valid ScriptBlock" - } + process { - # if we're using sessions, ensure sessions have been setup - if (!$Sessionless -and !(Test-PodeSessionsEnabled)) { - throw 'Sessions are required to use session persistent authentication' + $pipelineItemCount++ } - # set the file path if not passed - if ([string]::IsNullOrWhiteSpace($FilePath)) { - $FilePath = Join-PodeServerRoot -Folder '.' -FilePath 'users.json' - } - else { - $FilePath = Get-PodeRelativePath -Path $FilePath -JoinRoot -Resolve - } + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + # ensure the name doesn't already exist + if (Test-PodeAuthExists -Name $Name) { + # Authentication method already defined: {0} + throw ($PodeLocale.authMethodAlreadyDefinedExceptionMessage -f $Name) + } - # ensure the user file exists - if (!(Test-PodePath -Path $FilePath -NoStatus -FailOnDirectory)) { - throw "The user file does not exist: $($FilePath)" - } + # ensure the Scheme contains a scriptblock + if (Test-PodeIsEmpty $Scheme.ScriptBlock) { + # The supplied scheme for the '{0}' authentication validator requires a valid ScriptBlock. + throw ($PodeLocale.schemeRequiresValidScriptBlockExceptionMessage -f $Name) + } - # if we have a scriptblock, deal with using vars - if ($null -ne $ScriptBlock) { - $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState - } + # if we're using sessions, ensure sessions have been setup + if (!$Sessionless -and !(Test-PodeSessionsEnabled)) { + # Sessions are required to use session persistent authentication + throw ($PodeLocale.sessionsRequiredForSessionPersistentAuthExceptionMessage) + } - # add Windows AD auth method to server - $PodeContext.Server.Authentications.Methods[$Name] = @{ - Name = $Name - Scheme = $Scheme - ScriptBlock = (Get-PodeAuthUserFileMethod) - Arguments = @{ - FilePath = $FilePath - Users = $Users - Groups = $Groups - HmacSecret = $HmacSecret - ScriptBlock = @{ - Script = $ScriptBlock - UsingVariables = $usingVars - } + # set the file path if not passed + if ([string]::IsNullOrWhiteSpace($FilePath)) { + $FilePath = Join-PodeServerRoot -Folder '.' -FilePath 'users.json' } - Sessionless = $Sessionless - Failure = @{ - Url = $FailureUrl - Message = $FailureMessage + else { + $FilePath = Get-PodeRelativePath -Path $FilePath -JoinRoot -Resolve } - Success = @{ - Url = $SuccessUrl - UseOrigin = $SuccessUseOrigin + + # ensure the user file exists + if (!(Test-PodePath -Path $FilePath -NoStatus -FailOnDirectory)) { + # The user file does not exist: {0} + throw ($PodeLocale.userFileDoesNotExistExceptionMessage -f $FilePath) + } + + # if we have a scriptblock, deal with using vars + if ($null -ne $ScriptBlock) { + $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + } + + # add Windows AD auth method to server + $PodeContext.Server.Authentications.Methods[$Name] = @{ + Name = $Name + Scheme = $Scheme + ScriptBlock = (Get-PodeAuthUserFileMethod) + Arguments = @{ + FilePath = $FilePath + Users = $Users + Groups = $Groups + HmacSecret = $HmacSecret + ScriptBlock = @{ + Script = $ScriptBlock + UsingVariables = $usingVars + } + } + Sessionless = $Sessionless + Failure = @{ + Url = $FailureUrl + Message = $FailureMessage + } + Success = @{ + Url = $SuccessUrl + UseOrigin = $SuccessUseOrigin + } + Cache = @{} + Merged = $false + Parent = $null } - Cache = @{} - Merged = $false - Parent = $null } } @@ -2003,58 +2098,75 @@ function Add-PodeAuthWindowsLocal { [switch] $SuccessUseOrigin ) - - # ensure we're on Windows! - if (!(Test-PodeIsWindows)) { - throw 'Windows Local Authentication support is for Windows only' + begin { + $pipelineItemCount = 0 } - # ensure the name doesn't already exist - if (Test-PodeAuthExists -Name $Name) { - throw "Windows Local Authentication method already defined: $($Name)" - } + process { - # ensure the Scheme contains a scriptblock - if (Test-PodeIsEmpty $Scheme.ScriptBlock) { - throw "The supplied Scheme for the '$($Name)' Windows Local authentication validator requires a valid ScriptBlock" + $pipelineItemCount++ } - # if we're using sessions, ensure sessions have been setup - if (!$Sessionless -and !(Test-PodeSessionsEnabled)) { - throw 'Sessions are required to use session persistent authentication' - } + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + # ensure we're on Windows! + if (!(Test-PodeIsWindows)) { + # Windows Local Authentication support is for Windows only + throw ($PodeLocale.windowsLocalAuthSupportIsForWindowsOnlyExceptionMessage) + } - # if we have a scriptblock, deal with using vars - if ($null -ne $ScriptBlock) { - $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState - } + # ensure the name doesn't already exist + if (Test-PodeAuthExists -Name $Name) { + # Authentication method already defined: {0} + throw ($PodeLocale.authMethodAlreadyDefinedExceptionMessage -f $Name) + } - # add Windows Local auth method to server - $PodeContext.Server.Authentications.Methods[$Name] = @{ - Name = $Name - Scheme = $Scheme - ScriptBlock = (Get-PodeAuthWindowsLocalMethod) - Arguments = @{ - Users = $Users - Groups = $Groups - NoGroups = $NoGroups - ScriptBlock = @{ - Script = $ScriptBlock - UsingVariables = $usingVars - } + # ensure the Scheme contains a scriptblock + if (Test-PodeIsEmpty $Scheme.ScriptBlock) { + # The supplied scheme for the '{0}' authentication validator requires a valid ScriptBlock. + throw ($PodeLocale.schemeRequiresValidScriptBlockExceptionMessage -f $Name) } - Sessionless = $Sessionless - Failure = @{ - Url = $FailureUrl - Message = $FailureMessage + + # if we're using sessions, ensure sessions have been setup + if (!$Sessionless -and !(Test-PodeSessionsEnabled)) { + # Sessions are required to use session persistent authentication + throw ($PodeLocale.sessionsRequiredForSessionPersistentAuthExceptionMessage) } - Success = @{ - Url = $SuccessUrl - UseOrigin = $SuccessUseOrigin + + # if we have a scriptblock, deal with using vars + if ($null -ne $ScriptBlock) { + $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + } + + # add Windows Local auth method to server + $PodeContext.Server.Authentications.Methods[$Name] = @{ + Name = $Name + Scheme = $Scheme + ScriptBlock = (Get-PodeAuthWindowsLocalMethod) + Arguments = @{ + Users = $Users + Groups = $Groups + NoGroups = $NoGroups + ScriptBlock = @{ + Script = $ScriptBlock + UsingVariables = $usingVars + } + } + Sessionless = $Sessionless + Failure = @{ + Url = $FailureUrl + Message = $FailureMessage + } + Success = @{ + Url = $SuccessUrl + UseOrigin = $SuccessUseOrigin + } + Cache = @{} + Merged = $false + Parent = $null } - Cache = @{} - Merged = $false - Parent = $null } } @@ -2082,6 +2194,7 @@ ConvertTo-PodeJwt -Header @{ alg = 'hs256' } -Payload @{ sub = '123'; name = 'Jo #> function ConvertTo-PodeJwt { [CmdletBinding()] + [OutputType([string])] param( [Parameter(Mandatory = $true)] [hashtable] @@ -2097,7 +2210,8 @@ function ConvertTo-PodeJwt { # validate header if ([string]::IsNullOrWhiteSpace($Header.alg)) { - throw 'No algorithm supplied in JWT Header' + # No algorithm supplied in JWT Header + throw ($PodeLocale.noAlgorithmInJwtHeaderExceptionMessage) } # convert the header @@ -2143,6 +2257,7 @@ ConvertFrom-PodeJwt -Token "eyJ0eXAiOiJKV1QiLCJhbGciOiJoczI1NiJ9.eyJleHAiOjE2MjI #> function ConvertFrom-PodeJwt { [CmdletBinding(DefaultParameterSetName = 'Secret')] + [OutputType([pscustomobject])] param( [Parameter(Mandatory = $true)] [string] @@ -2161,13 +2276,15 @@ function ConvertFrom-PodeJwt { # check number of parts (should be 3) if ($parts.Length -ne 3) { - throw 'Invalid JWT supplied' + # Invalid JWT supplied + throw ($PodeLocale.invalidJwtSuppliedExceptionMessage) } # convert to header $header = ConvertFrom-PodeJwtBase64Value -Value $parts[0] if ([string]::IsNullOrWhiteSpace($header.alg)) { - throw 'Invalid JWT header algorithm supplied' + # Invalid JWT header algorithm supplied + throw ($PodeLocale.invalidJwtHeaderAlgorithmSuppliedExceptionMessage) } # convert to payload @@ -2184,15 +2301,18 @@ function ConvertFrom-PodeJwt { $isNoneAlg = ($header.alg -ieq 'none') if ([string]::IsNullOrWhiteSpace($signature) -and !$isNoneAlg) { - throw "No JWT signature supplied for $($header.alg)" + # No JWT signature supplied for {0} + throw ($PodeLocale.noJwtSignatureForAlgorithmExceptionMessage -f $header.alg) } if (![string]::IsNullOrWhiteSpace($signature) -and $isNoneAlg) { - throw 'Expected no JWT signature to be supplied' + # Expected no JWT signature to be supplied + throw ($PodeLocale.expectedNoJwtSignatureSuppliedExceptionMessage) } if ($isNoneAlg -and ($null -ne $Secret) -and ($Secret.Length -gt 0)) { - throw "Expected a signed JWT, 'none' algorithm is not allowed" + # Expected no JWT signature to be supplied + throw ($PodeLocale.expectedNoJwtSignatureSuppliedExceptionMessage) } if ($isNoneAlg) { @@ -2208,7 +2328,8 @@ function ConvertFrom-PodeJwt { $sig = New-PodeJwtSignature -Algorithm $header.alg -Token $sig -SecretBytes $Secret if ($sig -ne $parts[2]) { - throw 'Invalid JWT signature supplied' + # Invalid JWT signature supplied + throw ($PodeLocale.invalidJwtSignatureSuppliedExceptionMessage) } # it's valid return the payload! @@ -2247,14 +2368,16 @@ function Test-PodeJwt { # validate expiry if (![string]::IsNullOrWhiteSpace($Payload.exp)) { if ($now -gt $unixStart.AddSeconds($Payload.exp)) { - throw 'The JWT has expired' + # The JWT has expired + throw ($PodeLocale.jwtExpiredExceptionMessage) } } # validate not-before if (![string]::IsNullOrWhiteSpace($Payload.nbf)) { if ($now -lt $unixStart.AddSeconds($Payload.nbf)) { - throw 'The JWT is not yet valid for use' + # The JWT is not yet valid for use + throw ($PodeLocale.jwtNotYetValidExceptionMessage) } } } @@ -2357,54 +2480,69 @@ function ConvertFrom-PodeOIDCDiscovery { [switch] $UsePKCE ) - - # get the discovery doc - if (!$Url.EndsWith('/.well-known/openid-configuration')) { - $Url += '/.well-known/openid-configuration' + begin { + $pipelineItemCount = 0 } - $config = Invoke-RestMethod -Method Get -Uri $Url + process { - # check it supports the code response_type - if ($config.response_types_supported -inotcontains 'code') { - throw "The OAuth2 provider does not support the 'code' response_type" + $pipelineItemCount++ } - # can we have an InnerScheme? - if (($null -ne $InnerScheme) -and ($config.grant_types_supported -inotcontains 'password')) { - throw "The OAuth2 provider does not support the 'password' grant_type required by using an InnerScheme" - } + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + # get the discovery doc + if (!$Url.EndsWith('/.well-known/openid-configuration')) { + $Url += '/.well-known/openid-configuration' + } - # scopes - $scopes = $config.scopes_supported + $config = Invoke-RestMethod -Method Get -Uri $Url - if (($null -ne $Scope) -and ($Scope.Length -gt 0)) { - $scopes = @(foreach ($s in $Scope) { - if ($s -iin $config.scopes_supported) { - $s - } - }) - } + # check it supports the code response_type + if ($config.response_types_supported -inotcontains 'code') { + # The OAuth2 provider does not support the 'code' response_type + throw ($PodeLocale.oauth2ProviderDoesNotSupportCodeResponseTypeExceptionMessage) + } - # pkce code challenge method - $codeMethod = 'S256' - if ($config.code_challenge_methods_supported -inotcontains $codeMethod) { - $codeMethod = 'plain' - } + # can we have an InnerScheme? + if (($null -ne $InnerScheme) -and ($config.grant_types_supported -inotcontains 'password')) { + # The OAuth2 provider does not support the 'password' grant_type required by using an InnerScheme + throw ($PodeLocale.oauth2ProviderDoesNotSupportPasswordGrantTypeExceptionMessage) + } - return New-PodeAuthScheme ` - -OAuth2 ` - -ClientId $ClientId ` - -ClientSecret $ClientSecret ` - -AuthoriseUrl $config.authorization_endpoint ` - -TokenUrl $config.token_endpoint ` - -UserUrl $config.userinfo_endpoint ` - -RedirectUrl $RedirectUrl ` - -Scope $scopes ` - -InnerScheme $InnerScheme ` - -Middleware $Middleware ` - -CodeChallengeMethod $codeMethod ` - -UsePKCE:$UsePKCE + # scopes + $scopes = $config.scopes_supported + + if (($null -ne $Scope) -and ($Scope.Length -gt 0)) { + $scopes = @(foreach ($s in $Scope) { + if ($s -iin $config.scopes_supported) { + $s + } + }) + } + + # pkce code challenge method + $codeMethod = 'S256' + if ($config.code_challenge_methods_supported -inotcontains $codeMethod) { + $codeMethod = 'plain' + } + + return New-PodeAuthScheme ` + -OAuth2 ` + -ClientId $ClientId ` + -ClientSecret $ClientSecret ` + -AuthoriseUrl $config.authorization_endpoint ` + -TokenUrl $config.token_endpoint ` + -UserUrl $config.userinfo_endpoint ` + -RedirectUrl $RedirectUrl ` + -Scope $scopes ` + -InnerScheme $InnerScheme ` + -Middleware $Middleware ` + -CodeChallengeMethod $codeMethod ` + -UsePKCE:$UsePKCE + } } <# @@ -2422,6 +2560,7 @@ if (Test-PodeAuthUser) { ... } #> function Test-PodeAuthUser { [CmdletBinding()] + [OutputType([boolean])] param( [switch] $IgnoreSession diff --git a/src/Public/AutoImport.ps1 b/src/Public/AutoImport.ps1 index 4421700c1..9e1f1a60d 100644 --- a/src/Public/AutoImport.ps1 +++ b/src/Public/AutoImport.ps1 @@ -20,7 +20,7 @@ function Export-PodeModule { ) $PodeContext.Server.AutoImport.Modules.ExportList += @($Name) - $PodeContext.Server.AutoImport.Modules.ExportList = $PodeContext.Server.AutoImport.Modules.ExportList | Sort-Object -Unique + $PodeContext.Server.AutoImport.Modules.ExportList = @($PodeContext.Server.AutoImport.Modules.ExportList | Sort-Object -Unique) } <# @@ -46,11 +46,12 @@ function Export-PodeSnapin { # if non-windows or core, fail if ((Test-PodeIsPSCore) -or (Test-PodeIsUnix)) { - throw 'Snapins are only supported on Windows PowerShell' + # Snapins are only supported on Windows PowerShell + throw ($PodeLocale.snapinsSupportedOnWindowsPowershellOnlyExceptionMessage) } $PodeContext.Server.AutoImport.Snapins.ExportList += @($Name) - $PodeContext.Server.AutoImport.Snapins.ExportList = $PodeContext.Server.AutoImport.Snapins.ExportList | Sort-Object -Unique + $PodeContext.Server.AutoImport.Snapins.ExportList = @($PodeContext.Server.AutoImport.Snapins.ExportList | Sort-Object -Unique) } <# @@ -75,7 +76,7 @@ function Export-PodeFunction { ) $PodeContext.Server.AutoImport.Functions.ExportList += @($Name) - $PodeContext.Server.AutoImport.Functions.ExportList = $PodeContext.Server.AutoImport.Functions.ExportList | Sort-Object -Unique + $PodeContext.Server.AutoImport.Functions.ExportList = @($PodeContext.Server.AutoImport.Functions.ExportList | Sort-Object -Unique) } <# @@ -108,5 +109,5 @@ function Export-PodeSecretVault { ) $PodeContext.Server.AutoImport.SecretVaults[$Type].ExportList += @($Name) - $PodeContext.Server.AutoImport.SecretVaults[$Type].ExportList = $PodeContext.Server.AutoImport.SecretVaults[$Type].ExportList | Sort-Object -Unique + $PodeContext.Server.AutoImport.SecretVaults[$Type].ExportList = @($PodeContext.Server.AutoImport.SecretVaults[$Type].ExportList | Sort-Object -Unique) } \ No newline at end of file diff --git a/src/Public/Caching.ps1 b/src/Public/Caching.ps1 index 30fff5c41..3be0951d8 100644 --- a/src/Public/Caching.ps1 +++ b/src/Public/Caching.ps1 @@ -57,7 +57,8 @@ function Get-PodeCache { } # storage not found! - throw "Cache storage with name '$($Storage)' not found when attempting to retrieve cached item '$($Key)'" + # Cache storage with name not found when attempting to retrieve cached item + throw ($PodeLocale.cacheStorageNotFoundForRetrieveExceptionMessage -f $Storage, $Key) } <# @@ -104,7 +105,7 @@ function Set-PodeCache { [string] $Key, - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [object] $InputObject, @@ -117,29 +118,47 @@ function Set-PodeCache { $Storage = $null ) - # use the global settable default here - if ($Ttl -le 0) { - $Ttl = $PodeContext.Server.Cache.DefaultTtl + begin { + # Initialize an array to hold piped-in values + $pipelineValue = @() } - # inmem or custom storage? - if ([string]::IsNullOrEmpty($Storage)) { - $Storage = $PodeContext.Server.Cache.DefaultStorage - } - - # use inmem cache - if ([string]::IsNullOrEmpty($Storage)) { - Set-PodeCacheInternal -Key $Key -InputObject $InputObject -Ttl $Ttl + process { + # Add the current piped-in value to the array + $pipelineValue += $_ } - # used custom storage - elseif (Test-PodeCacheStorage -Key $Storage) { - $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Set -Arguments @($Key, $InputObject, $Ttl) -Splat - } - - # storage not found! - else { - throw "Cache storage with name '$($Storage)' not found when attempting to set cached item '$($Key)'" + end { + # If there are multiple piped-in values, set InputObject to the array of values + if ($pipelineValue.Count -gt 1) { + $InputObject = $pipelineValue + } + + # use the global settable default here + if ($Ttl -le 0) { + $Ttl = $PodeContext.Server.Cache.DefaultTtl + } + + # inmem or custom storage? + if ([string]::IsNullOrEmpty($Storage)) { + $Storage = $PodeContext.Server.Cache.DefaultStorage + } + + # use inmem cache + if ([string]::IsNullOrEmpty($Storage)) { + Set-PodeCacheInternal -Key $Key -InputObject $InputObject -Ttl $Ttl + } + + # used custom storage + elseif (Test-PodeCacheStorage -Key $Storage) { + $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Set -Arguments @($Key, $InputObject, $Ttl) -Splat + } + + # storage not found! + else { + # Cache storage with name not found when attempting to set cached item + throw ($PodeLocale.cacheStorageNotFoundForSetExceptionMessage -f $Storage, $Key) + } } } @@ -190,7 +209,8 @@ function Test-PodeCache { } # storage not found! - throw "Cache storage with name '$($Storage)' not found when attempting to check if cached item '$($Key)' exists" + # Cache storage with name not found when attempting to check if cached item exists + throw ($PodeLocale.cacheStorageNotFoundForExistsExceptionMessage -f $Storage, $Key) } <# @@ -241,7 +261,8 @@ function Remove-PodeCache { # storage not found! else { - throw "Cache storage with name '$($Storage)' not found when attempting to remove cached item '$($Key)'" + # Cache storage with name not found when attempting to remove cached item + throw ($PodeLocale.cacheStorageNotFoundForRemoveExceptionMessage -f $Storage, $Key) } } @@ -286,7 +307,8 @@ function Clear-PodeCache { # storage not found! else { - throw "Cache storage with name '$($Storage)' not found when attempting to clear cached" + # Cache storage with name not found when attempting to clear the cache + throw ($PodeLocale.cacheStorageNotFoundForClearExceptionMessage -f $Storage) } } @@ -355,7 +377,8 @@ function Add-PodeCacheStorage { # test if storage already exists if (Test-PodeCacheStorage -Name $Name) { - throw "Cache Storage with name '$($Name) already exists" + # Cache Storage with name already exists + throw ($PodeLocale.cacheStorageAlreadyExistsExceptionMessage -f $Name) } # add cache storage diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index 0b121bd25..3d609f763 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -130,109 +130,130 @@ function Start-PodeServer { [switch] $EnableBreakpoints ) + begin { + $pipelineItemCount = 0 + } - # ensure the session is clean - $PodeContext = $null - $ShowDoneMessage = $true + process { + $pipelineItemCount++ + } - try { - # if we have a filepath, resolve it - and extract a root path from it - if ($PSCmdlet.ParameterSetName -ieq 'file') { - $FilePath = Get-PodeRelativePath -Path $FilePath -Resolve -TestPath -JoinRoot -RootPath $MyInvocation.PSScriptRoot + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } # Store the name of the current runspace + $previousRunspaceName = Get-PodeCurrentRunspaceName + # Sets the name of the current runspace + Set-PodeCurrentRunspaceName -Name 'PodeServer' - # if not already supplied, set root path - if ([string]::IsNullOrWhiteSpace($RootPath)) { - if ($CurrentPath) { - $RootPath = $PWD.Path - } - else { - $RootPath = Split-Path -Parent -Path $FilePath + # ensure the session is clean + $PodeContext = $null + $ShowDoneMessage = $true + + try { + # if we have a filepath, resolve it - and extract a root path from it + if ($PSCmdlet.ParameterSetName -ieq 'file') { + $FilePath = Get-PodeRelativePath -Path $FilePath -Resolve -TestPath -JoinRoot -RootPath $MyInvocation.PSScriptRoot + + # if not already supplied, set root path + if ([string]::IsNullOrWhiteSpace($RootPath)) { + if ($CurrentPath) { + $RootPath = $PWD.Path + } + else { + $RootPath = Split-Path -Parent -Path $FilePath + } } } - } - # configure the server's root path - if (!(Test-PodeIsEmpty $RootPath)) { - $RootPath = Get-PodeRelativePath -Path $RootPath -RootPath $MyInvocation.PSScriptRoot -JoinRoot -Resolve -TestPath - } + # configure the server's root path + if (!(Test-PodeIsEmpty $RootPath)) { + $RootPath = Get-PodeRelativePath -Path $RootPath -RootPath $MyInvocation.PSScriptRoot -JoinRoot -Resolve -TestPath + } - # create main context object - $PodeContext = New-PodeContext ` - -ScriptBlock $ScriptBlock ` - -FilePath $FilePath ` - -Threads $Threads ` - -Interval $Interval ` - -ServerRoot (Protect-PodeValue -Value $RootPath -Default $MyInvocation.PSScriptRoot) ` - -ServerlessType $ServerlessType ` - -ListenerType $ListenerType ` - -EnablePool $EnablePool ` - -StatusPageExceptions $StatusPageExceptions ` - -DisableTermination:$DisableTermination ` - -Quiet:$Quiet ` - -EnableBreakpoints:$EnableBreakpoints - - # set it so ctrl-c can terminate, unless serverless/iis, or disabled - if (!$PodeContext.Server.DisableTermination -and ($null -eq $psISE)) { - [Console]::TreatControlCAsInput = $true - } + # create main context object + $PodeContext = New-PodeContext ` + -ScriptBlock $ScriptBlock ` + -FilePath $FilePath ` + -Threads $Threads ` + -Interval $Interval ` + -ServerRoot (Protect-PodeValue -Value $RootPath -Default $MyInvocation.PSScriptRoot) ` + -ServerlessType $ServerlessType ` + -ListenerType $ListenerType ` + -EnablePool $EnablePool ` + -StatusPageExceptions $StatusPageExceptions ` + -DisableTermination:$DisableTermination ` + -Quiet:$Quiet ` + -EnableBreakpoints:$EnableBreakpoints + + # set it so ctrl-c can terminate, unless serverless/iis, or disabled + if (!$PodeContext.Server.DisableTermination -and ($null -eq $psISE)) { + [Console]::TreatControlCAsInput = $true + } - # start the file monitor for interally restarting - Start-PodeFileMonitor + # start the file monitor for interally restarting + Start-PodeFileMonitor - # start the server - Start-PodeInternalServer -Request $Request -Browse:$Browse + # start the server + Start-PodeInternalServer -Request $Request -Browse:$Browse - # at this point, if it's just a one-one off script, return - if (!(Test-PodeServerKeepOpen)) { - return - } + # at this point, if it's just a one-one off script, return + if (!(Test-PodeServerKeepOpen)) { + return + } - # sit here waiting for termination/cancellation, or to restart the server - while (!(Test-PodeTerminationPressed -Key $key) -and !($PodeContext.Tokens.Cancellation.IsCancellationRequested)) { - Start-Sleep -Seconds 1 + # sit here waiting for termination/cancellation, or to restart the server + while (!(Test-PodeTerminationPressed -Key $key) -and !($PodeContext.Tokens.Cancellation.IsCancellationRequested)) { + Start-Sleep -Seconds 1 - # get the next key presses - $key = Get-PodeConsoleKey + # get the next key presses + $key = Get-PodeConsoleKey + + # check for internal restart + if (($PodeContext.Tokens.Restart.IsCancellationRequested) -or (Test-PodeRestartPressed -Key $key)) { + Restart-PodeInternalServer + } - # check for internal restart - if (($PodeContext.Tokens.Restart.IsCancellationRequested) -or (Test-PodeRestartPressed -Key $key)) { - Restart-PodeInternalServer + # check for open browser + if (Test-PodeOpenBrowserPressed -Key $key) { + Invoke-PodeEvent -Type Browser + Start-Process (Get-PodeEndpointUrl) + } } - # check for open browser - if (Test-PodeOpenBrowserPressed -Key $key) { - Invoke-PodeEvent -Type Browser - Start-Process (Get-PodeEndpointUrl) + if ($PodeContext.Server.IsIIS -and $PodeContext.Server.IIS.Shutdown) { + # (IIS Shutdown) + Write-PodeHost $PodeLocale.iisShutdownMessage -NoNewLine -ForegroundColor Yellow + Write-PodeHost ' ' -NoNewLine } + # Terminating... + Write-PodeHost $PodeLocale.terminatingMessage -NoNewLine -ForegroundColor Yellow + Invoke-PodeEvent -Type Terminate + $PodeContext.Tokens.Cancellation.Cancel() } - - if ($PodeContext.Server.IsIIS -and $PodeContext.Server.IIS.Shutdown) { - Write-PodeHost '(IIS Shutdown) ' -NoNewline -ForegroundColor Yellow + catch { + Invoke-PodeEvent -Type Crash + $ShowDoneMessage = $false + throw } + finally { + Invoke-PodeEvent -Type Stop - Write-PodeHost 'Terminating...' -NoNewline -ForegroundColor Yellow - Invoke-PodeEvent -Type Terminate - $PodeContext.Tokens.Cancellation.Cancel() - } - catch { - Invoke-PodeEvent -Type Crash - $ShowDoneMessage = $false - throw - } - finally { - Invoke-PodeEvent -Type Stop + # set output values + Set-PodeOutputVariable - # set output values - Set-PodeOutputVariables + # unregister secret vaults + Unregister-PodeSecretVaultsInternal - # unregister secret vaults - Unregister-PodeSecretVaults + # clean the runspaces and tokens + Close-PodeServerInternal -ShowDoneMessage:$ShowDoneMessage - # clean the runspaces and tokens - Close-PodeServerInternal -ShowDoneMessage:$ShowDoneMessage + # clean the session + $PodeContext = $null - # clean the session - $PodeContext = $null + # Restore the name of the current runspace + Set-PodeCurrentRunspaceName -Name $previousRunspaceName + } } } @@ -448,6 +469,7 @@ pode build pode start #> function Pode { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] [CmdletBinding()] param( [Parameter(Mandatory = $true)] @@ -490,7 +512,7 @@ function Pode { # quick check to see if the data is required if ($Action -ine 'init') { if ($null -eq $data) { - Write-Host 'package.json file not found' -ForegroundColor Red + Write-PodeHost 'package.json file not found' -ForegroundColor Red return } else { @@ -501,14 +523,14 @@ function Pode { } if ([string]::IsNullOrWhiteSpace($actionScript) -and $Action -ine 'install') { - Write-Host "package.json does not contain a script for the $($Action) action" -ForegroundColor Yellow + Write-PodeHost "package.json does not contain a script for the $($Action) action" -ForegroundColor Yellow return } } } else { if ($null -ne $data) { - Write-Host 'package.json already exists' -ForegroundColor Yellow + Write-PodeHost 'package.json already exists' -ForegroundColor Yellow return } } @@ -532,7 +554,7 @@ function Pode { if (![string]::IsNullOrWhiteSpace($v)) { $map.license = $v } $map | ConvertTo-Json -Depth 10 | Out-File -FilePath $file -Encoding utf8 -Force - Write-Host 'Success, saved package.json' -ForegroundColor Green + Write-PodeHost 'Success, saved package.json' -ForegroundColor Green } 'test' { @@ -545,10 +567,10 @@ function Pode { 'install' { if ($Dev) { - Install-PodeLocalModules -Modules $data.devModules + Install-PodeLocalModule -Module $data.devModules } - Install-PodeLocalModules -Modules $data.modules + Install-PodeLocalModule -Module $data.modules Invoke-PodePackageScript -ActionScript $actionScript } @@ -636,52 +658,68 @@ function Show-PodeGui { [switch] $HideFromTaskbar ) + begin { + $pipelineItemCount = 0 + } - # error if serverless - Test-PodeIsServerless -FunctionName 'Show-PodeGui' -ThrowError + process { - # only valid for Windows PowerShell - if ((Test-PodeIsPSCore) -and ($PSVersionTable.PSVersion.Major -eq 6)) { - throw 'Show-PodeGui is currently only available for Windows PowerShell, and PowerShell 7+ on Windows' + $pipelineItemCount++ } - # enable the gui and set general settings - $PodeContext.Server.Gui.Enabled = $true - $PodeContext.Server.Gui.Title = $Title - $PodeContext.Server.Gui.ShowInTaskbar = !$HideFromTaskbar - $PodeContext.Server.Gui.WindowState = $WindowState - $PodeContext.Server.Gui.WindowStyle = $WindowStyle - $PodeContext.Server.Gui.ResizeMode = $ResizeMode - - # set the window's icon path - if (![string]::IsNullOrWhiteSpace($Icon)) { - $PodeContext.Server.Gui.Icon = Get-PodeRelativePath -Path $Icon -JoinRoot -Resolve - if (!(Test-Path $PodeContext.Server.Gui.Icon)) { - throw "Path to icon for GUI does not exist: $($PodeContext.Server.Gui.Icon)" + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) } - } + # error if serverless + Test-PodeIsServerless -FunctionName 'Show-PodeGui' -ThrowError - # set the height of the window - $PodeContext.Server.Gui.Height = $Height - if ($PodeContext.Server.Gui.Height -le 0) { - $PodeContext.Server.Gui.Height = 'auto' - } + # only valid for Windows PowerShell + if ((Test-PodeIsPSCore) -and ($PSVersionTable.PSVersion.Major -eq 6)) { + # Show-PodeGui is currently only available for Windows PowerShell and PowerShell 7+ on Windows + throw ($PodeLocale.showPodeGuiOnlyAvailableOnWindowsExceptionMessage) + } - # set the width of the window - $PodeContext.Server.Gui.Width = $Width - if ($PodeContext.Server.Gui.Width -le 0) { - $PodeContext.Server.Gui.Width = 'auto' - } + # enable the gui and set general settings + $PodeContext.Server.Gui.Enabled = $true + $PodeContext.Server.Gui.Title = $Title + $PodeContext.Server.Gui.ShowInTaskbar = !$HideFromTaskbar + $PodeContext.Server.Gui.WindowState = $WindowState + $PodeContext.Server.Gui.WindowStyle = $WindowStyle + $PodeContext.Server.Gui.ResizeMode = $ResizeMode + + # set the window's icon path + if (![string]::IsNullOrWhiteSpace($Icon)) { + $PodeContext.Server.Gui.Icon = Get-PodeRelativePath -Path $Icon -JoinRoot -Resolve + if (!(Test-Path $PodeContext.Server.Gui.Icon)) { + # Path to icon for GUI does not exist + throw ($PodeLocale.pathToIconForGuiDoesNotExistExceptionMessage -f $PodeContext.Server.Gui.Icon) + } + } - # set the gui to use a specific listener - $PodeContext.Server.Gui.EndpointName = $EndpointName + # set the height of the window + $PodeContext.Server.Gui.Height = $Height + if ($PodeContext.Server.Gui.Height -le 0) { + $PodeContext.Server.Gui.Height = 'auto' + } - if (![string]::IsNullOrWhiteSpace($EndpointName)) { - if (!$PodeContext.Server.Endpoints.ContainsKey($EndpointName)) { - throw "Endpoint with name '$($EndpointName)' does not exist" + # set the width of the window + $PodeContext.Server.Gui.Width = $Width + if ($PodeContext.Server.Gui.Width -le 0) { + $PodeContext.Server.Gui.Width = 'auto' } - $PodeContext.Server.Gui.Endpoint = $PodeContext.Server.Endpoints[$EndpointName] + # set the gui to use a specific listener + $PodeContext.Server.Gui.EndpointName = $EndpointName + + if (![string]::IsNullOrWhiteSpace($EndpointName)) { + if (!$PodeContext.Server.Endpoints.ContainsKey($EndpointName)) { + # Endpoint with name '$EndpointName' does not exist. + throw ($PodeLocale.endpointNameNotExistExceptionMessage -f $EndpointName) + } + + $PodeContext.Server.Gui.Endpoint = $PodeContext.Server.Endpoints[$EndpointName] + } } } @@ -902,7 +940,8 @@ function Add-PodeEndpoint { # if RedirectTo is supplied, then a Name is mandatory if (![string]::IsNullOrWhiteSpace($RedirectTo) -and [string]::IsNullOrWhiteSpace($Name)) { - throw 'A Name is required for the endpoint if the RedirectTo parameter is supplied' + # A Name is required for the endpoint if the RedirectTo parameter is supplied + throw ($PodeLocale.nameRequiredForEndpointIfRedirectToSuppliedExceptionMessage) } # get the type of endpoint @@ -928,7 +967,8 @@ function Add-PodeEndpoint { # parse the endpoint for host/port info if (![string]::IsNullOrWhiteSpace($Hostname) -and !(Test-PodeHostname -Hostname $Hostname)) { - throw "Invalid hostname supplied: $($Hostname)" + # Invalid hostname supplied + throw ($PodeLocale.invalidHostnameSuppliedExceptionMessage -f $Hostname) } if ((Test-PodeHostname -Hostname $Address) -and ($Address -inotin @('localhost', 'all'))) { @@ -948,27 +988,32 @@ function Add-PodeEndpoint { } if ($PodeContext.Server.Endpoints.ContainsKey($Name)) { - throw "An endpoint with the name '$($Name)' has already been defined" + # An endpoint named has already been defined + throw ($PodeLocale.endpointAlreadyDefinedExceptionMessage -f $Name) } # protocol must be https for client certs, or hosted behind a proxy like iis if (($Protocol -ine 'https') -and !(Test-PodeIsHosted) -and $AllowClientCertificate) { - throw 'Client certificates are only supported on HTTPS endpoints' + # Client certificates are only supported on HTTPS endpoints + throw ($PodeLocale.clientCertificatesOnlySupportedOnHttpsEndpointsExceptionMessage) } # explicit tls is only supported for smtp/tcp if (($type -inotin @('smtp', 'tcp')) -and ($TlsMode -ieq 'explicit')) { - throw 'The Explicit TLS mode is only supported on SMTPS and TCPS endpoints' + # The Explicit TLS mode is only supported on SMTPS and TCPS endpoints + throw ($PodeLocale.explicitTlsModeOnlySupportedOnSmtpsTcpsEndpointsExceptionMessage) } # ack message is only for smtp/tcp if (($type -inotin @('smtp', 'tcp')) -and ![string]::IsNullOrEmpty($Acknowledge)) { - throw 'The Acknowledge message is only supported on SMTP and TCP endpoints' + # The Acknowledge message is only supported on SMTP and TCP endpoints + throw ($PodeLocale.acknowledgeMessageOnlySupportedOnSmtpTcpEndpointsExceptionMessage) } # crlf message end is only for tcp if (($type -ine 'tcp') -and $CRLFMessageEnd) { - throw 'The CRLF message end check is only supported on TCP endpoints' + # The CRLF message end check is only supported on TCP endpoints + throw ($PodeLocale.crlfMessageEndCheckOnlySupportedOnTcpEndpointsExceptionMessage) } # new endpoint object @@ -1007,7 +1052,7 @@ function Add-PodeEndpoint { # set ssl protocols if (!(Test-PodeIsEmpty $SslProtocol)) { - $obj.Ssl.Protocols = (ConvertTo-PodeSslProtocols -Protocols $SslProtocol) + $obj.Ssl.Protocols = (ConvertTo-PodeSslProtocol -Protocol $SslProtocol) } # set the ip for the context (force to localhost for IIS) @@ -1041,7 +1086,8 @@ function Add-PodeEndpoint { # if the address is non-local, then check admin privileges if (!$Force -and !(Test-PodeIPAddressLocal -IP $obj.Address) -and !(Test-PodeIsAdminUser)) { - throw 'Must be running with administrator priviledges to listen on non-localhost addresses' + # Must be running with administrator privileges to listen on non-localhost addresses + throw ($PodeLocale.mustBeRunningWithAdminPrivilegesExceptionMessage) } # has this endpoint been added before? (for http/https we can just not add it again) @@ -1053,7 +1099,8 @@ function Add-PodeEndpoint { if (!(Test-PodeIsHosted) -and ($PSCmdlet.ParameterSetName -ilike 'cert*')) { # fail if protocol is not https if (@('https', 'wss', 'smtps', 'tcps') -inotcontains $Protocol) { - throw 'Certificate supplied for non-HTTPS/WSS endpoint' + # Certificate supplied for non-HTTPS/WSS endpoint + throw ($PodeLocale.certificateSuppliedForNonHttpsWssEndpointExceptionMessage) } switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { @@ -1076,7 +1123,8 @@ function Add-PodeEndpoint { # fail if the cert is expired if ($obj.Certificate.Raw.NotAfter -lt [datetime]::Now) { - throw "The certificate '$($obj.Certificate.Raw.Subject)' has expired: $($obj.Certificate.Raw.NotAfter)" + # The certificate has expired + throw ($PodeLocale.certificateExpiredExceptionMessage -f $obj.Certificate.Raw.Subject, $obj.Certificate.Raw.NotAfter) } } @@ -1102,7 +1150,8 @@ function Add-PodeEndpoint { # ensure the name exists if (Test-PodeIsEmpty $redir_endpoint) { - throw "An endpoint with the name '$($RedirectTo)' has not been defined for redirecting" + # An endpoint named has not been defined for redirecting + throw ($PodeLocale.endpointNotDefinedForRedirectingExceptionMessage -f $RedirectTo) } # build the redirect route @@ -1310,7 +1359,8 @@ function Set-PodeDefaultFolder { $PodeContext.Server.DefaultFolders[$Type] = $Path } else { - throw "Folder $Path doesn't exist" + # Path does not exist + throw ($PodeLocale.pathNotExistExceptionMessage -f $Path) } } diff --git a/src/Public/Events.ps1 b/src/Public/Events.ps1 index fe03becab..b3f800a0d 100644 --- a/src/Public/Events.ps1 +++ b/src/Public/Events.ps1 @@ -43,7 +43,7 @@ function Register-PodeEvent { # error if already registered if (Test-PodeEvent -Type $Type -Name $Name) { - throw "$($Type) event already registered: $($Name)" + throw ($PodeLocale.eventAlreadyRegisteredExceptionMessage -f $Type, $Name) # "$($Type) event already registered: $($Name)" } # check for scoped vars @@ -89,7 +89,7 @@ function Unregister-PodeEvent { # error if not registered if (!(Test-PodeEvent -Type $Type -Name $Name)) { - throw "No $($Type) event registered: $($Name)" + throw ($PodeLocale.noEventRegisteredExceptionMessage -f $Type, $Name) # "No $($Type) event registered: $($Name)" } # remove event @@ -202,6 +202,7 @@ Use-PodeEvents Use-PodeEvents -Path './my-events' #> function Use-PodeEvents { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param( [Parameter()] diff --git a/src/Public/FileWatchers.ps1 b/src/Public/FileWatchers.ps1 index cce9fbd28..e1cda2023 100644 --- a/src/Public/FileWatchers.ps1 +++ b/src/Public/FileWatchers.ps1 @@ -118,31 +118,33 @@ function Add-PodeFileWatcher { # resolve path if relative if (!(Test-PodeIsPSCore)) { - $Path = Convert-PodePlaceholders -Path $Path -Prepend '%' -Append '%' + $Path = Convert-PodePlaceholder -Path $Path -Prepend '%' -Append '%' } $Path = Get-PodeRelativePath -Path $Path -JoinRoot -Resolve if (!(Test-PodeIsPSCore)) { - $Path = Convert-PodePlaceholders -Path $Path -Pattern '\%(?[\w]+)\%' -Prepend ':' -Append ([string]::Empty) + $Path = Convert-PodePlaceholder -Path $Path -Pattern '\%(?[\w]+)\%' -Prepend ':' -Append ([string]::Empty) } # resolve path, and test it - $hasPlaceholders = Test-PodePlaceholders -Path $Path + $hasPlaceholders = Test-PodePlaceholder -Path $Path if ($hasPlaceholders) { - $rgxPath = Update-PodeRouteSlashes -Path $Path -NoLeadingSlash - $rgxPath = Resolve-PodePlaceholders -Path $rgxPath -Slashes + $rgxPath = Update-PodeRouteSlash -Path $Path -NoLeadingSlash + $rgxPath = Resolve-PodePlaceholder -Path $rgxPath -Slashes $Path = $Path -ireplace (Get-PodePlaceholderRegex), '*' } # test path to make sure it exists if (!(Test-PodePath $Path -NoStatus)) { - throw "The path does not exist: $($Path)" + # Path does not exist + throw ($PodeLocale.pathNotExistExceptionMessage -f $Path) } # test if we have the file watcher already if (Test-PodeFileWatcher -Name $Name) { - throw "A File Watcher with the name '$($Name)' has already been defined" + # A File Watcher named has already been defined + throw ($PodeLocale.fileWatcherAlreadyDefinedExceptionMessage -f $Name) } # if we have a file path supplied, load that path as a scriptblock @@ -290,6 +292,7 @@ Removes all File Watchers. Clear-PodeFileWatchers #> function Clear-PodeFileWatchers { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param() @@ -313,6 +316,7 @@ Use-PodeFileWatchers Use-PodeFileWatchers -Path './my-watchers' #> function Use-PodeFileWatchers { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param( [Parameter()] diff --git a/src/Public/Flash.ps1 b/src/Public/Flash.ps1 index ca005ea8c..0c9364d70 100644 --- a/src/Public/Flash.ps1 +++ b/src/Public/Flash.ps1 @@ -29,7 +29,8 @@ function Add-PodeFlashMessage { # if sessions haven't been setup, error if (!(Test-PodeSessionsEnabled)) { - throw 'Sessions are required to use Flash messages' + # Sessions are required to use Flash messages + throw ($PodeLocale.sessionsRequiredForFlashMessagesExceptionMessage) } # append the message against the key @@ -56,12 +57,14 @@ Clears all of the flash messages currently stored in the session. Clear-PodeFlashMessages #> function Clear-PodeFlashMessages { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param() # if sessions haven't been setup, error if (!(Test-PodeSessionsEnabled)) { - throw 'Sessions are required to use Flash messages' + # Sessions are required to use Flash messages + throw ($PodeLocale.sessionsRequiredForFlashMessagesExceptionMessage) } # clear all keys @@ -86,7 +89,7 @@ Get-PodeFlashMessage -Name 'error' #> function Get-PodeFlashMessage { [CmdletBinding()] - [OutputType([string[]])] + [OutputType([System.Object[]])] param( [Parameter(Mandatory = $true)] [string] @@ -95,7 +98,8 @@ function Get-PodeFlashMessage { # if sessions haven't been setup, error if (!(Test-PodeSessionsEnabled)) { - throw 'Sessions are required to use Flash messages' + # Sessions are required to use Flash messages + throw ($PodeLocale.sessionsRequiredForFlashMessagesExceptionMessage) } # retrieve messages from session, then delete it @@ -124,13 +128,15 @@ Returns all of the names for each of the messages currently being stored. This d Get-PodeFlashMessageNames #> function Get-PodeFlashMessageNames { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] - [OutputType([string[]])] + [OutputType([System.Object[]])] param() # if sessions haven't been setup, error if (!(Test-PodeSessionsEnabled)) { - throw 'Sessions are required to use Flash messages' + # Sessions are required to use Flash messages + throw ($PodeLocale.sessionsRequiredForFlashMessagesExceptionMessage) } # return list of all current keys @@ -164,7 +170,8 @@ function Remove-PodeFlashMessage { # if sessions haven't been setup, error if (!(Test-PodeSessionsEnabled)) { - throw 'Sessions are required to use Flash messages' + # Sessions are required to use Flash messages + throw ($PodeLocale.sessionsRequiredForFlashMessagesExceptionMessage) } # remove key from flash messages @@ -197,7 +204,8 @@ function Test-PodeFlashMessage { # if sessions haven't been setup, error if (!(Test-PodeSessionsEnabled)) { - throw 'Sessions are required to use Flash messages' + # Sessions are required to use Flash messages + throw ($PodeLocale.sessionsRequiredForFlashMessagesExceptionMessage) } # return if a key exists as a flash message diff --git a/src/Public/Handlers.ps1 b/src/Public/Handlers.ps1 index eb418f2f0..244570397 100644 --- a/src/Public/Handlers.ps1 +++ b/src/Public/Handlers.ps1 @@ -59,7 +59,8 @@ function Add-PodeHandler { # ensure handler isn't already set if ($PodeContext.Server.Handlers[$Type].ContainsKey($Name)) { - throw "[$($Type)] $($Name): Handler already defined" + # [Type] Name: Handler already defined + throw ($PodeLocale.handlerAlreadyDefinedExceptionMessage -f $Type, $Name) } # if we have a file path supplied, load that path as a scriptblock @@ -131,6 +132,7 @@ The Type of Handlers to remove. Clear-PodeHandlers -Type Smtp #> function Clear-PodeHandlers { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param( [Parameter()] @@ -166,6 +168,7 @@ Use-PodeHandlers Use-PodeHandlers -Path './my-handlers' #> function Use-PodeHandlers { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param( [Parameter()] diff --git a/src/Public/Logging.ps1 b/src/Public/Logging.ps1 index 801002f7b..727a1fcd3 100644 --- a/src/Public/Logging.ps1 +++ b/src/Public/Logging.ps1 @@ -106,7 +106,8 @@ function New-PodeLoggingMethod { [Parameter(ParameterSetName = 'File')] [ValidateScript({ if ($_ -lt 0) { - throw "MaxDays must be 0 or greater, but got: $($_)s" + # MaxDays must be 0 or greater, but got + throw ($PodeLocale.maxDaysInvalidExceptionMessage -f $MaxDays) } return $true @@ -117,7 +118,8 @@ function New-PodeLoggingMethod { [Parameter(ParameterSetName = 'File')] [ValidateScript({ if ($_ -lt 0) { - throw "MaxSize must be 0 or greater, but got: $($_)s" + # MaxSize must be 0 or greater, but got + throw ($PodeLocale.maxSizeInvalidExceptionMessage -f $MaxSize) } return $true @@ -132,7 +134,8 @@ function New-PodeLoggingMethod { [Parameter(Mandatory = $true, ParameterSetName = 'Custom')] [ValidateScript({ if (Test-PodeIsEmpty $_) { - throw 'A non-empty ScriptBlock is required for the Custom logging output method' + # A non-empty ScriptBlock is required for the Custom logging output method + throw ($PodeLocale.nonEmptyScriptBlockRequiredForCustomLoggingExceptionMessage) } return $true @@ -187,7 +190,8 @@ function New-PodeLoggingMethod { 'eventviewer' { # only windows if (!(Test-PodeIsWindows)) { - throw 'Event Viewer logging only supported on Windows' + # Event Viewer logging only supported on Windows + throw ($PodeLocale.eventViewerLoggingSupportedOnWindowsOnlyExceptionMessage) } # create source @@ -260,12 +264,14 @@ function Enable-PodeRequestLogging { # error if it's already enabled if ($PodeContext.Server.Logging.Types.Contains($name)) { - throw 'Request Logging has already been enabled' + # Request Logging has already been enabled + throw ($PodeLocale.requestLoggingAlreadyEnabledExceptionMessage) } # ensure the Method contains a scriptblock if (Test-PodeIsEmpty $Method.ScriptBlock) { - throw 'The supplied output Method for Request Logging requires a valid ScriptBlock' + # The supplied output Method for Request Logging requires a valid ScriptBlock + throw ($PodeLocale.loggingMethodRequiresValidScriptBlockExceptionMessage -f 'Request') } # username property @@ -343,12 +349,14 @@ function Enable-PodeErrorLogging { # error if it's already enabled if ($PodeContext.Server.Logging.Types.Contains($name)) { - throw 'Error Logging has already been enabled' + # Error Logging has already been enabled + throw ($PodeLocale.errorLoggingAlreadyEnabledExceptionMessage) } # ensure the Method contains a scriptblock if (Test-PodeIsEmpty $Method.ScriptBlock) { - throw 'The supplied output Method for Error Logging requires a valid ScriptBlock' + # The supplied output Method for Error Logging requires a valid ScriptBlock + throw ($PodeLocale.loggingMethodRequiresValidScriptBlockExceptionMessage -f 'Error') } # all errors? @@ -420,7 +428,8 @@ function Add-PodeLogger { [Parameter(Mandatory = $true)] [ValidateScript({ if (Test-PodeIsEmpty $_) { - throw 'A non-empty ScriptBlock is required for the logging method' + # A non-empty ScriptBlock is required for the logging method + throw ($PodeLocale.nonEmptyScriptBlockRequiredForLoggingMethodExceptionMessage) } return $true @@ -435,12 +444,14 @@ function Add-PodeLogger { # ensure the name doesn't already exist if ($PodeContext.Server.Logging.Types.ContainsKey($Name)) { - throw "Logging method already defined: $($Name)" + # Logging method already defined + throw ($PodeLocale.loggingMethodAlreadyDefinedExceptionMessage -f $Name) } # ensure the Method contains a scriptblock if (Test-PodeIsEmpty $Method.ScriptBlock) { - throw "The supplied output Method for the '$($Name)' Logging method requires a valid ScriptBlock" + # The supplied output Method for the Logging method requires a valid ScriptBlock + throw ($PodeLocale.loggingMethodRequiresValidScriptBlockExceptionMessage -f $Name) } # check for scoped vars @@ -490,6 +501,7 @@ Clears all Logging methods that have been configured. Clear-PodeLoggers #> function Clear-PodeLoggers { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param() @@ -550,7 +562,7 @@ function Write-PodeErrorLog { } # do nothing if the error level isn't present - $levels = @(Get-PodeErrorLoggingLevels) + $levels = @(Get-PodeErrorLoggingLevel) if ($levels -inotcontains $Level) { return } @@ -648,6 +660,7 @@ $value = Protect-PodeLogItem -Item 'Username=Morty, Password=Hunter2' #> function Protect-PodeLogItem { [CmdletBinding()] + [OutputType([string])] param( [Parameter(ValueFromPipeline = $true)] [string] diff --git a/src/Public/Metrics.ps1 b/src/Public/Metrics.ps1 index f30787caf..0d47243d4 100644 --- a/src/Public/Metrics.ps1 +++ b/src/Public/Metrics.ps1 @@ -16,6 +16,7 @@ $totalUptime = Get-PodeServerUptime -Total #> function Get-PodeServerUptime { [CmdletBinding()] + [OutputType([long])] param( [switch] $Total @@ -70,6 +71,7 @@ $404Reqs = Get-PodeServerRequestMetric -StatusCode 404 #> function Get-PodeServerRequestMetric { [CmdletBinding(DefaultParameterSetName = 'StatusCode')] + [OutputType([long])] param( [Parameter(ParameterSetName = 'StatusCode')] [int] @@ -90,7 +92,7 @@ function Get-PodeServerRequestMetric { $strCode = "$($StatusCode)" if (!$PodeContext.Metrics.Requests.StatusCodes.ContainsKey($strCode)) { - return 0 + return 0L } return $PodeContext.Metrics.Requests.StatusCodes[$strCode] diff --git a/src/Public/Middleware.ps1 b/src/Public/Middleware.ps1 index 6ee06f80f..8b7f8d15d 100644 --- a/src/Public/Middleware.ps1 +++ b/src/Public/Middleware.ps1 @@ -142,7 +142,8 @@ function New-PodeCsrfToken { # fail if the csrf logic hasn't been initialised if (!(Test-PodeCsrfConfigured)) { - throw 'CSRF Middleware has not been initialised' + # CSRF Middleware has not been initialized + throw ($PodeLocale.csrfMiddlewareNotInitializedExceptionMessage) } # generate a new secret and salt @@ -171,7 +172,8 @@ function Get-PodeCsrfMiddleware { # fail if the csrf logic hasn't been initialised if (!(Test-PodeCsrfConfigured)) { - throw 'CSRF Middleware has not been initialised' + # CSRF Middleware has not been initialized + throw ($PodeLocale.csrfMiddlewareNotInitializedExceptionMessage) } # return scriptblock for the csrf route middleware to test tokens @@ -239,7 +241,8 @@ function Initialize-PodeCsrf { # if sessions haven't been setup and we're not using cookies, error if (!$UseCookies -and !(Test-PodeSessionsEnabled)) { - throw 'Sessions are required to use CSRF unless you want to use cookies' + # Sessions are required to use CSRF unless you want to use cookies + throw ($PodeLocale.sessionsRequiredForCsrfExceptionMessage) } # if we're using cookies, ensure a global secret exists @@ -247,7 +250,8 @@ function Initialize-PodeCsrf { $Secret = (Protect-PodeValue -Value $Secret -Default (Get-PodeCookieSecret -Global)) if (Test-PodeIsEmpty $Secret) { - throw "When using cookies for CSRF, a Secret is required. You can either supply a Secret, or set the Cookie global secret - (Set-PodeCookieSecret '' -Global)" + # When using cookies for CSRF, a Secret is required + throw ($PodeLocale.csrfCookieRequiresSecretExceptionMessage) } } @@ -355,18 +359,31 @@ function Add-PodeBodyParser { [scriptblock] $ScriptBlock ) + begin { + $pipelineItemCount = 0 + } - # if a parser for the type already exists, fail - if ($PodeContext.Server.BodyParsers.ContainsKey($ContentType)) { - throw "There is already a body parser defined for the $($ContentType) content-type" + process { + $pipelineItemCount++ } - # check for scoped vars - $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + # if a parser for the type already exists, fail + if ($PodeContext.Server.BodyParsers.ContainsKey($ContentType)) { + # A body-parser is already defined for the content-type + throw ($PodeLocale.bodyParserAlreadyDefinedForContentTypeExceptionMessage -f $ContentType) + } + + # check for scoped vars + $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState - $PodeContext.Server.BodyParsers[$ContentType] = @{ - ScriptBlock = $ScriptBlock - UsingVariables = $usingVars + $PodeContext.Server.BodyParsers[$ContentType] = @{ + ScriptBlock = $ScriptBlock + UsingVariables = $usingVars + } } } @@ -392,12 +409,14 @@ function Remove-PodeBodyParser { $ContentType ) - # if there's no parser for the type, return - if (!$PodeContext.Server.BodyParsers.ContainsKey($ContentType)) { - return - } + process { + # if there's no parser for the type, return + if (!$PodeContext.Server.BodyParsers.ContainsKey($ContentType)) { + return + } - $null = $PodeContext.Server.BodyParsers.Remove($ContentType) + $null = $PodeContext.Server.BodyParsers.Remove($ContentType) + } } <# @@ -442,7 +461,7 @@ function Add-PodeMiddleware { [scriptblock] $ScriptBlock, - [Parameter(Mandatory = $true, ParameterSetName = 'Input', ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ParameterSetName = 'Input')] [hashtable] $InputObject, @@ -454,36 +473,51 @@ function Add-PodeMiddleware { [object[]] $ArgumentList ) - - # ensure name doesn't already exist - if (($PodeContext.Server.Middleware | Where-Object { $_.Name -ieq $Name } | Measure-Object).Count -gt 0) { - throw "[Middleware] $($Name): Middleware already defined" + begin { + $pipelineItemCount = 0 } - # if it's a script - call New-PodeMiddleware - if ($PSCmdlet.ParameterSetName -ieq 'script') { - $InputObject = (New-PodeMiddlewareInternal ` - -ScriptBlock $ScriptBlock ` - -Route $Route ` - -ArgumentList $ArgumentList ` - -PSSession $PSCmdlet.SessionState) - } - else { - $Route = ConvertTo-PodeRouteRegex -Path $Route - $InputObject.Route = Protect-PodeValue -Value $Route -Default $InputObject.Route - $InputObject.Options = Protect-PodeValue -Value $Options -Default $InputObject.Options + process { + $pipelineItemCount++ } - # ensure we have a script to run - if (Test-PodeIsEmpty $InputObject.Logic) { - throw '[Middleware]: No logic supplied in ScriptBlock' - } + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + # ensure name doesn't already exist + if (($PodeContext.Server.Middleware | Where-Object { $_.Name -ieq $Name } | Measure-Object).Count -gt 0) { + # [Middleware] Name: Middleware already defined + throw ($PodeLocale.middlewareAlreadyDefinedExceptionMessage -f $Name) - # set name, and override route/args - $InputObject.Name = $Name + } - # add the logic to array of middleware that needs to be run - $PodeContext.Server.Middleware += $InputObject + # if it's a script - call New-PodeMiddleware + if ($PSCmdlet.ParameterSetName -ieq 'script') { + $InputObject = (New-PodeMiddlewareInternal ` + -ScriptBlock $ScriptBlock ` + -Route $Route ` + -ArgumentList $ArgumentList ` + -PSSession $PSCmdlet.SessionState) + } + else { + $Route = ConvertTo-PodeRouteRegex -Path $Route + $InputObject.Route = Protect-PodeValue -Value $Route -Default $InputObject.Route + $InputObject.Options = Protect-PodeValue -Value $Options -Default $InputObject.Options + } + + # ensure we have a script to run + if (Test-PodeIsEmpty $InputObject.Logic) { + # [Middleware]: No logic supplied in ScriptBlock + throw ($PodeLocale.middlewareNoLogicSuppliedExceptionMessage) + } + + # set name, and override route/args + $InputObject.Name = $Name + + # add the logic to array of middleware that needs to be run + $PodeContext.Server.Middleware += $InputObject + } } <# @@ -512,7 +546,7 @@ function New-PodeMiddleware { [CmdletBinding()] [OutputType([hashtable])] param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [scriptblock] $ScriptBlock, @@ -524,12 +558,24 @@ function New-PodeMiddleware { [object[]] $ArgumentList ) + begin { + $pipelineItemCount = 0 + } + + process { + $pipelineItemCount++ + } - return New-PodeMiddlewareInternal ` - -ScriptBlock $ScriptBlock ` - -Route $Route ` - -ArgumentList $ArgumentList ` - -PSSession $PSCmdlet.SessionState + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + return New-PodeMiddlewareInternal ` + -ScriptBlock $ScriptBlock ` + -Route $Route ` + -ArgumentList $ArgumentList ` + -PSSession $PSCmdlet.SessionState + } } <# diff --git a/src/Public/OAComponents.ps1 b/src/Public/OAComponents.ps1 index 6a9cf4f62..3bbc6eab4 100644 --- a/src/Public/OAComponents.ps1 +++ b/src/Public/OAComponents.ps1 @@ -80,7 +80,7 @@ function Add-PodeOAComponentResponse { ) $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag foreach ($tag in $DefinitionTag) { - $PodeContext.Server.OpenAPI.Definitions[$tag].components.responses[$Name] = New-PodeOResponseInternal -DefinitionTag $tag -Params $PSBoundParameters + $PodeContext.Server.OpenAPI.Definitions[$tag].components.responses[$Name] = New-PodeOResponseInternal -DefinitionTag $tag -Params $PSBoundParameters } } @@ -129,7 +129,7 @@ function Add-PodeOAComponentSchema { [string] $Name, - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [Alias('Schema')] [hashtable] $Component, @@ -140,34 +140,45 @@ function Add-PodeOAComponentSchema { [string[]] $DefinitionTag ) + begin { + $pipelineItemCount = 0 + } - $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag + process { + $pipelineItemCount++ + } - foreach ($tag in $DefinitionTag) { - $PodeContext.Server.OpenAPI.Definitions[$tag].components.schemas[$Name] = ($Component | ConvertTo-PodeOASchemaProperty -DefinitionTag $tag) - if ($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.schemaValidation) { - try { - $modifiedComponent = ($Component | ConvertTo-PodeOASchemaProperty -DefinitionTag $tag) | Resolve-PodeOAReference -DefinitionTag $tag - $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.schemaJson[$Name] = @{ - 'available' = $true - 'schema' = $modifiedComponent - 'json' = $modifiedComponent | ConvertTo-Json -depth $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.depth - } - } - catch { - if ($_.ToString().StartsWith('Validation of schema with')) { + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag + + foreach ($tag in $DefinitionTag) { + $PodeContext.Server.OpenAPI.Definitions[$tag].components.schemas[$Name] = ($Component | ConvertTo-PodeOASchemaProperty -DefinitionTag $tag) + if ($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.schemaValidation) { + try { + $modifiedComponent = ($Component | ConvertTo-PodeOASchemaProperty -DefinitionTag $tag) | Resolve-PodeOAReference -DefinitionTag $tag $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.schemaJson[$Name] = @{ - 'available' = $false + 'available' = $true + 'schema' = $modifiedComponent + 'json' = $modifiedComponent | ConvertTo-Json -Depth $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.depth + } + } + catch { + if ($_.ToString().StartsWith('Validation of schema with')) { + $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.schemaJson[$Name] = @{ + 'available' = $false + } } } } - } - if ($Description) { - $PodeContext.Server.OpenAPI.Definitions[$tag].components.schemas[$Name].description = $Description + if ($Description) { + $PodeContext.Server.OpenAPI.Definitions[$tag].components.schemas[$Name].description = $Description + } } } - } @@ -219,26 +230,37 @@ function Add-PodeOAComponentHeader { [string] $Description, - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [hashtable] $Schema, [string[]] $DefinitionTag ) + begin { + $pipelineItemCount = 0 + } - $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag + process { + $pipelineItemCount++ + } - foreach ($tag in $DefinitionTag) { - $param = [ordered]@{ - 'schema' = ($Schema | ConvertTo-PodeOASchemaProperty -NoDescription -DefinitionTag $tag) + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) } - if ( $Description) { - $param['description'] = $Description + $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag + + foreach ($tag in $DefinitionTag) { + $param = [ordered]@{ + 'schema' = ($Schema | ConvertTo-PodeOASchemaProperty -NoDescription -DefinitionTag $tag) + } + if ( $Description) { + $param['description'] = $Description + } + $PodeContext.Server.OpenAPI.Definitions[$tag].components.headers[$Name] = $param } - $PodeContext.Server.OpenAPI.Definitions[$tag].components.headers[$Name] = $param } - } @@ -292,7 +314,7 @@ function Add-PodeOAComponentRequestBody { [string] $Name, - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [Alias('ContentSchemas')] [hashtable] $Content, @@ -308,20 +330,32 @@ function Add-PodeOAComponentRequestBody { [string[]] $DefinitionTag ) + begin { + $pipelineItemCount = 0 + } - $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag - - foreach ($tag in $DefinitionTag) { - $param = [ordered]@{ content = ($Content | ConvertTo-PodeOAObjectSchema -DefinitionTag $tag) } + process { + $pipelineItemCount++ + } - if ($Required.IsPresent) { - $param['required'] = $Required.IsPresent + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) } + $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag + + foreach ($tag in $DefinitionTag) { + $param = [ordered]@{ content = ($Content | ConvertTo-PodeOAObjectSchema -DefinitionTag $tag) } + + if ($Required.IsPresent) { + $param['required'] = $Required.IsPresent + } - if ( $Description) { - $param['description'] = $Description + if ( $Description) { + $param['description'] = $Description + } + $PodeContext.Server.OpenAPI.Definitions[$tag].components.requestBodies[$Name] = $param } - $PodeContext.Server.OpenAPI.Definitions[$tag].components.requestBodies[$Name] = $param } } @@ -356,6 +390,7 @@ You can use this tag to reference the specific API documentation, schema, or ver .EXAMPLE New-PodeOAIntProperty -Name 'userId' | ConvertTo-PodeOAParameter -In Query | Add-PodeOAComponentParameter -Name 'UserIdParam' #> + function Add-PodeOAComponentParameter { [CmdletBinding()] param( @@ -364,28 +399,41 @@ function Add-PodeOAComponentParameter { [string] $Name, - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [hashtable] $Parameter, [string[]] $DefinitionTag ) - - $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag + begin { + $pipelineItemCount = 0 + } - foreach ($tag in $DefinitionTag) { - if ([string]::IsNullOrWhiteSpace($Name)) { - if ($Parameter.name) { - $Name = $Parameter.name - } - else { - throw 'The Parameter has no name. Please provide a name to this component using -Name property' + process { + $pipelineItemCount++ + } + + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + + $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag + + foreach ($tag in $DefinitionTag) { + if ([string]::IsNullOrWhiteSpace($Name)) { + if ($Parameter.name) { + $Name = $Parameter.name + } + else { + # The Parameter has no name. Please provide a name to this component using the `Name` parameter + throw ($PodeLocale.parameterHasNoNameExceptionMessage) + } } + $PodeContext.Server.OpenAPI.Definitions[$tag].components.parameters[$Name] = $Parameter } - $PodeContext.Server.OpenAPI.Definitions[$tag].components.parameters[$Name] = $Parameter } - } <# @@ -602,7 +650,7 @@ You can use this tag to reference the specific API documentation, schema, or ver Add-PodeOAComponentCallBack -Title 'test' -Path '{$request.body#/id}' -Method Post ` -RequestBody (New-PodeOARequestBody -Content @{'*/*' = (New-PodeOAStringProperty -Name 'id')}) ` -Response ( - New-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentMediaType 'application/json','application/xml' -Content 'Pet' -Array) + New-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json','application/xml' -Content 'Pet' -Array) New-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' | New-PodeOAResponse -StatusCode 404 -Description 'Pet not found' | New-PodeOAResponse -Default -Description 'Something is wrong' @@ -726,26 +774,30 @@ function Add-PodeOAComponentPathItem { $DefinitionTag ) - $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag + $_definitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag $refRoute = @{ Method = $Method.ToLower() NotPrepared = $true OpenApi = @{ - Responses = $null - Parameters = $null - RequestBody = $null - callbacks = [ordered]@{} - Authentication = @() + Responses = $null + Parameters = $null + RequestBody = $null + callbacks = [ordered]@{} + Authentication = @() + Servers = @() + DefinitionTag = $_definitionTag + IsDefTagConfigured = ($null -ne $DefinitionTag) #Definition Tag has been configured (Not default) } } - foreach ($tag in $DefinitionTag) { + foreach ($tag in $_definitionTag) { if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $tag ) { - throw 'The feature reusable component pathItems is not available in OpenAPI v3.0.x' + # The 'pathItems' reusable component feature is not available in OpenAPI v3.0. + throw ($PodeLocale.reusableComponentPathItemsNotAvailableInOpenApi30ExceptionMessage) } #add the default OpenApi responses if ( $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.defaultResponses) { - $refRoute.OpenApi.Responses = $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.defaultResponses.Clone() + $refRoute.OpenApi.Responses = Copy-PodeObjectDeepClone -InputObject $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.defaultResponses } $PodeContext.Server.OpenAPI.Definitions[$tag].components.pathItems[$Name] = $refRoute } @@ -782,8 +834,8 @@ Test-PodeOAVersion -Version 3.1 -DefinitionTag 'default' function Test-PodeOAVersion { param ( [Parameter(Mandatory = $true)] - [ValidateSet( 3.1 , 3.0 )] - [decimal] + [ValidateSet( '3.1' , '3.0' )] + [string] $Version, [Parameter(Mandatory = $true)] @@ -890,7 +942,7 @@ function Remove-PodeOAComponent { } if (!(Test-Path Alias:Enable-PodeOpenApiViewer)) { - New-Alias Enable-PodeOpenApiViewer -Value Enable-PodeOAViewer + New-Alias Enable-PodeOpenApiViewer -Value Enable-PodeOAViewer } if (!(Test-Path Alias:Enable-PodeOA)) { @@ -900,4 +952,3 @@ if (!(Test-Path Alias:Enable-PodeOA)) { if (!(Test-Path Alias:Get-PodeOpenApiDefinition)) { New-Alias Get-PodeOpenApiDefinition -Value Get-PodeOADefinition } - diff --git a/src/Public/OAProperties.ps1 b/src/Public/OAProperties.ps1 index 06f65e502..8c16b8bb1 100644 --- a/src/Public/OAProperties.ps1 +++ b/src/Public/OAProperties.ps1 @@ -174,7 +174,7 @@ New-PodeOAMultiTypeProperty -Name 'password' -type string,object -Format Passwor function New-PodeOAMultiTypeProperty { [CmdletBinding(DefaultParameterSetName = 'Inbuilt')] param( - [Parameter(ValueFromPipeline = $true, DontShow = $true )] + [Parameter(ValueFromPipeline = $true, Position = 0, DontShow = $true )] [hashtable[]] $ParamsList, @@ -349,7 +349,8 @@ function New-PodeOAMultiTypeProperty { if ($type -contains 'object') { if ($NoProperties) { if ($Properties -or $MinProperties -or $MaxProperties) { - throw '-NoProperties is not compatible with -Properties, -MinProperties and -MaxProperties' + # The parameter 'NoProperties' is mutually exclusive with 'Properties', 'MinProperties' and 'MaxProperties' + throw ($PodeLocale.noPropertiesMutuallyExclusiveExceptionMessage) } $param.properties = @($null) } @@ -360,7 +361,7 @@ function New-PodeOAMultiTypeProperty { $param.properties = @() } if ($DiscriminatorProperty) { - $param.discriminator = @{ + $param.discriminator = [ordered]@{ 'propertyName' = $DiscriminatorProperty } if ($DiscriminatorMapping) { @@ -368,7 +369,8 @@ function New-PodeOAMultiTypeProperty { } } elseif ($DiscriminatorMapping) { - throw 'Parameter -DiscriminatorMapping requires the -DiscriminatorProperty parameters' + # The parameter 'DiscriminatorMapping' can only be used when 'DiscriminatorProperty' is present + throw ($PodeLocale.discriminatorMappingRequiresDiscriminatorPropertyExceptionMessage) } } if ($type -contains 'boolean') { @@ -377,7 +379,8 @@ function New-PodeOAMultiTypeProperty { $param.default = $Default } else { - throw "The default value is not a boolean and it's not part of the enum" + # The default value is not a boolean and is not part of the enum + throw ($PodeLocale.defaultValueNotBooleanOrEnumExceptionMessage) } } } @@ -398,6 +401,7 @@ function New-PodeOAMultiTypeProperty { } } } + <# .SYNOPSIS Creates a new OpenAPI integer property. @@ -538,7 +542,7 @@ function New-PodeOAIntProperty { [CmdletBinding(DefaultParameterSetName = 'Inbuilt')] [OutputType([System.Collections.Specialized.OrderedDictionary])] param( - [Parameter(ValueFromPipeline = $true, DontShow = $true)] + [Parameter(ValueFromPipeline = $true, Position = 0, DontShow = $true)] [hashtable[]] $ParamsList, @@ -797,7 +801,7 @@ New-PodeOANumberProperty -Name 'gravity' -Default 9.8 function New-PodeOANumberProperty { [CmdletBinding(DefaultParameterSetName = 'Inbuilt')] param( - [Parameter(ValueFromPipeline = $true, DontShow = $true )] + [Parameter(ValueFromPipeline = $true, Position = 0, DontShow = $true )] [hashtable[]] $ParamsList, @@ -1052,7 +1056,7 @@ New-PodeOAStringProperty -Name 'password' -Format Password function New-PodeOAStringProperty { [CmdletBinding(DefaultParameterSetName = 'Inbuilt')] param( - [Parameter(ValueFromPipeline = $true, DontShow = $true )] + [Parameter(ValueFromPipeline = $true, Position = 0, DontShow = $true )] [hashtable[]] $ParamsList, @@ -1298,7 +1302,7 @@ function New-PodeOABoolProperty { [CmdletBinding(DefaultParameterSetName = 'Inbuilt')] param( - [Parameter(ValueFromPipeline = $true, DontShow = $true)] + [Parameter(ValueFromPipeline = $true, Position = 0, DontShow = $true)] [hashtable[]] $ParamsList, @@ -1396,7 +1400,8 @@ function New-PodeOABoolProperty { $param.default = $Default } else { - throw "The default value is not a boolean and it's not part of the enum" + # The default value is not a boolean and is not part of the enum + throw ($PodeLocale.defaultValueNotBooleanOrEnumExceptionMessage) } } @@ -1535,15 +1540,15 @@ New-PodeOAObjectProperty -Name 'user' -Properties @('') .EXAMPLE New-PodeOABoolProperty -Name 'enabled' -Required| New-PodeOAObjectProperty -Name 'extraProperties' -AdditionalProperties [ordered]@{ - "property1" = @{ "type" = "string"; "description" = "Description for property1" }; - "property2" = @{ "type" = "integer"; "format" = "int32" } + "property1" = [ordered]@{ "type" = "string"; "description" = "Description for property1" }; + "property2" = [ordered]@{ "type" = "integer"; "format" = "int32" } } #> function New-PodeOAObjectProperty { [CmdletBinding(DefaultParameterSetName = 'Inbuilt')] param( - [Parameter(ValueFromPipeline = $true, DontShow = $true , Position = 0 )] + [Parameter(ValueFromPipeline = $true, Position = 0, DontShow = $true )] [hashtable[]] $ParamsList, @@ -1645,9 +1650,9 @@ function New-PodeOAObjectProperty { $param = New-PodeOAPropertyInternal -type 'object' -Params $PSBoundParameters if ($NoProperties) { if ($Properties -or $MinProperties -or $MaxProperties) { - throw '-NoProperties is not compatible with -Properties, -MinProperties and -MaxProperties' + # The parameter `NoProperties` is mutually exclusive with `Properties`, `MinProperties` and `MaxProperties` + throw ($PodeLocale.noPropertiesMutuallyExclusiveExceptionMessage) } - $param.properties = @($null) $PropertiesFromPipeline = $false } elseif ($Properties) { @@ -1659,7 +1664,7 @@ function New-PodeOAObjectProperty { $PropertiesFromPipeline = $true } if ($DiscriminatorProperty) { - $param.discriminator = @{ + $param.discriminator = [ordered]@{ 'propertyName' = $DiscriminatorProperty } if ($DiscriminatorMapping) { @@ -1667,7 +1672,8 @@ function New-PodeOAObjectProperty { } } elseif ($DiscriminatorMapping) { - throw 'Parameter -DiscriminatorMapping requires the -DiscriminatorProperty parameters' + # The parameter 'DiscriminatorMapping' can only be used when 'DiscriminatorProperty' is present + throw ($PodeLocale.discriminatorMappingRequiresDiscriminatorPropertyExceptionMessage) } $collectedInput = [System.Collections.Generic.List[hashtable]]::new() } @@ -1728,27 +1734,41 @@ This string value represents the property in the payload that indicates which sp It's essential in scenarios where an API endpoint handles data that conforms to one of several derived schemas from a common base schema. .PARAMETER DiscriminatorMapping -If supplied, define a mapping between the values of the discriminator property and the corresponding subtype schemas. +If supplied, defines a mapping between the values of the discriminator property and the corresponding subtype schemas. This parameter accepts a HashTable where each key-value pair maps a discriminator value to a specific subtype schema name. It's used in conjunction with the -DiscriminatorProperty to provide complete discrimination logic in polymorphic scenarios. -.EXAMPLE -Add-PodeOAComponentSchema -Name 'Pets' -Component ( Merge-PodeOAProperty -Type OneOf -ObjectDefinitions @( 'Cat','Dog') -Discriminator "petType") +.PARAMETER NoObjectDefinitionsFromPipeline +Prevents object definitions from being used in the computation but still passes them through the pipeline. +.PARAMETER Name +Specifies the name of the OpenAPI object. + +.PARAMETER Required +Indicates if the object is required. + +.PARAMETER Description +Provides a description for the OpenAPI object. + +.EXAMPLE +Add-PodeOAComponentSchema -Name 'Pets' -Component (Merge-PodeOAProperty -Type OneOf -ObjectDefinitions @('Cat', 'Dog') -Discriminator "petType") .EXAMPLE Add-PodeOAComponentSchema -Name 'Cat' -Component ( - Merge-PodeOAProperty -Type AllOf -ObjectDefinitions @( 'Pet', ( New-PodeOAObjectProperty -Properties @( - (New-PodeOAStringProperty -Name 'huntingSkill' -Description 'The measured skill for hunting' -Enum @( 'clueless', 'lazy', 'adventurous', 'aggressive')) - )) + Merge-PodeOAProperty -Type AllOf -ObjectDefinitions @( + 'Pet', + (New-PodeOAObjectProperty -Properties @( + (New-PodeOAStringProperty -Name 'huntingSkill' -Description 'The measured skill for hunting' -Enum @('clueless', 'lazy', 'adventurous', 'aggressive')) )) + ) +) #> function Merge-PodeOAProperty { [CmdletBinding(DefaultParameterSetName = 'Inbuilt')] [OutputType([System.Collections.Specialized.OrderedDictionary])] param( - [Parameter(ValueFromPipeline = $true, DontShow = $true )] + [Parameter(ValueFromPipeline = $true, Position = 0, DontShow = $true )] [hashtable[]] $ParamsList, @@ -1765,11 +1785,28 @@ function Merge-PodeOAProperty { $DiscriminatorProperty, [hashtable] - $DiscriminatorMapping + $DiscriminatorMapping, + + [switch] + $NoObjectDefinitionsFromPipeline, + + [Parameter(Mandatory = $true, ParameterSetName = 'Name')] + [string] + $Name, + + [Parameter( ParameterSetName = 'Name')] + [switch] + $Required, + + [Parameter( ParameterSetName = 'Name')] + [string] + $Description ) begin { - + # Initialize an ordered dictionary $param = [ordered]@{} + + # Set the type of validation switch ($type.ToLower()) { 'oneof' { $param.type = 'oneOf' @@ -1782,21 +1819,43 @@ function Merge-PodeOAProperty { } } + # Add name to the parameter dictionary if provided + if ($Name) { + $param.name = $Name + } + + # Add description to the parameter dictionary if provided + if ($Description) { + $param.description = $Description + } + + # Set the required field if the switch is present + if ($Required.IsPresent) { + $param.required = $Required.IsPresent + } + + # Initialize schemas array $param.schemas = @() + + # Add object definitions to the schemas array if ($ObjectDefinitions) { foreach ($schema in $ObjectDefinitions) { if ($schema -is [System.Object[]] -or ($schema -is [hashtable] -and (($schema.type -ine 'object') -and !$schema.object))) { - throw "Only properties of type Object can be associated with $($param.type)" + # Only properties of type Object can be associated with $param.type + throw ($PodeLocale.propertiesTypeObjectAssociationExceptionMessage -f $param.type) } $param.schemas += $schema } } + + # Add discriminator property and mapping if provided if ($DiscriminatorProperty) { if ($type.ToLower() -eq 'allof' ) { - throw 'Discriminator parameter is not compatible with allOf' + # The parameter 'Discriminator' is incompatible with `allOf` + throw ($PodeLocale.discriminatorIncompatibleWithAllOfExceptionMessage) } - $param.discriminator = @{ + $param.discriminator = [ordered]@{ 'propertyName' = $DiscriminatorProperty } if ($DiscriminatorMapping) { @@ -1804,21 +1863,35 @@ function Merge-PodeOAProperty { } } elseif ($DiscriminatorMapping) { - throw 'Parameter -DiscriminatorMapping requires the -DiscriminatorProperty parameters' + # The parameter 'DiscriminatorMapping' can only be used when 'DiscriminatorProperty' is present + throw ($PodeLocale.discriminatorMappingRequiresDiscriminatorPropertyExceptionMessage) } + # Initialize a list to collect input from the pipeline + $collectedInput = [System.Collections.Generic.List[hashtable]]::new() } process { if ($ParamsList) { - if ($ParamsList.type -ine 'object' -and !$ParamsList.object) { - throw "Only properties of type Object can be associated with $type" + if ($NoObjectDefinitionsFromPipeline) { + # Add to collected input if the switch is present + $collectedInput.AddRange($ParamsList) + } + else { + # Add to schemas if the switch is not present + $param.schemas += $ParamsList } - $param.schemas += $ParamsList } } end { - return $param + if ($NoObjectDefinitionsFromPipeline) { + # Return collected input and param dictionary if switch is present + return $collectedInput + $param + } + else { + # Return the param dictionary + return $param + } } } @@ -2024,4 +2097,4 @@ function New-PodeOAComponentSchemaProperty { if (!(Test-Path Alias:New-PodeOASchemaProperty)) { New-Alias New-PodeOASchemaProperty -Value New-PodeOAComponentSchemaProperty -} +} \ No newline at end of file diff --git a/src/Public/OpenApi.ps1 b/src/Public/OpenApi.ps1 index 70c13b7f1..4a07a4d29 100644 --- a/src/Public/OpenApi.ps1 +++ b/src/Public/OpenApi.ps1 @@ -118,9 +118,11 @@ function Enable-PodeOpenApi { [string] $RouteFilter = '/*', + [Parameter()] [string[]] $EndpointName, + [Parameter()] [object[]] $Middleware, @@ -144,21 +146,26 @@ function Enable-PodeOpenApi { [switch] $RestrictRoutes, + [Parameter()] [ValidateSet('View', 'Download')] [String] $Mode = 'view', + [Parameter()] [ValidateSet('Json', 'Json-Compress', 'Yaml')] [String] $MarkupLanguage = 'Json', + [Parameter()] [switch] $EnableSchemaValidation, + [Parameter()] [ValidateRange(1, 100)] [int] $Depth = 20, + [Parameter()] [switch] $DisableMinimalDefinitions, @@ -170,6 +177,7 @@ function Enable-PodeOpenApi { [switch] $NoDefaultResponses, + [Parameter()] [string] $DefinitionTag @@ -181,9 +189,10 @@ function Enable-PodeOpenApi { if (! $Version) { $Version = '0.0.0' } - Write-PodeHost -ForegroundColor Yellow "WARNING: Title, Version, and Description on 'Enable-PodeOpenApi' are deprecated. Please use 'Add-PodeOAInfo' instead." + # WARNING: Title, Version, and Description on 'Enable-PodeOpenApi' are deprecated. Please use 'Add-PodeOAInfo' instead + Write-PodeHost $PodeLocale.deprecatedTitleVersionDescriptionWarningMessage -ForegroundColor Yellow } - if ( $DefinitionTag -ine $PodeContext.Server.OpenAPI.DefaultDefinitionTag ) { + if ( $DefinitionTag -ine $PodeContext.Server.Web.OpenApi.DefaultDefinitionTag) { $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag] = Get-PodeOABaseObject } $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.enableMinimalDefinitions = !$DisableMinimalDefinitions.IsPresent @@ -224,7 +233,8 @@ function Enable-PodeOpenApi { $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.schemaValidation = $EnableSchemaValidation.IsPresent } else { - throw 'Schema validation required Powershell version 6.1.0 or greater' + # Schema validation required PowerShell version 6.1.0 or greater + throw ($PodeLocale.schemaValidationRequiresPowerShell610ExceptionMessage) } } @@ -268,7 +278,7 @@ function Enable-PodeOpenApi { return } - if (($mode -ieq 'download') ) { + if ($mode -ieq 'download') { # Set-PodeResponseAttachment -Path Add-PodeHeader -Name 'Content-Disposition' -Value "attachment; filename=openapi.$format" } @@ -282,14 +292,14 @@ function Enable-PodeOpenApi { # write the openapi definition if ($format -ieq 'yaml') { if ($mode -ieq 'view') { - Write-PodeTextResponse -Value (ConvertTo-PodeYaml -InputObject $def -depth $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth) -ContentType 'text/x-yaml; charset=utf-8' + Write-PodeTextResponse -Value (ConvertTo-PodeYaml -InputObject $def -depth $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth) -ContentType 'application/yaml; charset=utf-8' #Changed to be RFC 9512 compliant } else { - Write-PodeYamlResponse -Value $def -depth $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth + Write-PodeYamlResponse -Value $def -Depth $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth } } else { - Write-PodeJsonResponse -Value $def -depth $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth -NoCompress:$meta.NoCompress + Write-PodeJsonResponse -Value $def -Depth $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth -NoCompress:$meta.NoCompress } } @@ -308,12 +318,16 @@ function Enable-PodeOpenApi { #set new DefaultResponses if ($NoDefaultResponses.IsPresent) { - $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.defaultResponses = @{} + $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.defaultResponses = [ordered]@{} } elseif ($DefaultResponses) { $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.defaultResponses = $DefaultResponses } $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.enabled = $true + + if ($EndpointName) { + $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.EndpointName = $EndpointName + } } @@ -355,7 +369,7 @@ Add-PodeOAServerEndpoint -Url "https://{username}.gigantic-server.com:{port}/{ba description = 'this value is assigned by the service provider, in this example gigantic-server.com' } port = @{ - enum = @('System.Object[]') # Assuming 'System.Object[]' is a placeholder for actual values + enum = @('System.Object[]') # Assuming 'System.Object[]' is a placeholder for actual values default = 8443 } basePath = @{ @@ -488,14 +502,14 @@ function Get-PodeOADefinition { $meta.Description = $Description } - $oApi = Get-PodeOpenApiDefinitionInternal -MetaInfo $meta -EndpointName $WebEvent.Endpoint.Name -DefinitionTag $DefinitionTag + $oApi = Get-PodeOpenApiDefinitionInternal -MetaInfo $meta -EndpointName $WebEvent.Endpoint.Name -DefinitionTag $DefinitionTag switch ($Format.ToLower()) { 'json' { - return ConvertTo-Json -InputObject $oApi -depth $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth + return ConvertTo-Json -InputObject $oApi -Depth $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth } 'json-compress' { - return ConvertTo-Json -InputObject $oApi -depth $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth -Compress + return ConvertTo-Json -InputObject $oApi -Depth $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth -Compress } 'yaml' { return ConvertTo-PodeYaml -InputObject $oApi -depth $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth @@ -583,7 +597,7 @@ function Add-PodeOAResponse { [Alias('HeaderSchemas')] [AllowEmptyString()] [ValidateNotNullOrEmpty()] - [ValidateScript({ $_ -is [string] -or $_ -is [string[]] -or $_ -is [hashtable] -or $_ -is [ordered] })] + [ValidateScript({ $_ -is [string] -or $_ -is [string[]] -or $_ -is [hashtable] -or $_ -is [System.Collections.Specialized.OrderedDictionary] })] $Headers, [Parameter(Mandatory = $false, ParameterSetName = 'Schema')] @@ -612,32 +626,45 @@ function Add-PodeOAResponse { [string[]] $DefinitionTag ) - - if ($null -eq $Route) { throw 'Add-PodeOAResponse - The parameter -Route cannot be NULL.' } - - $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag - # override status code with default - if ($Default) { - $code = 'default' + begin { + # Initialize an array to hold piped-in values + $pipelineValue = @() } - else { - $code = "$($StatusCode)" + + process { + # Add the current piped-in value to the array + $pipelineValue += $_ } - # add the respones to the routes - foreach ($r in @($Route)) { - foreach ($tag in $DefinitionTag) { - if (! $r.OpenApi.Responses.$tag) { - $r.OpenApi.Responses.$tag = @{} + end { + # Set Route to the array of values + if ($pipelineValue.Count -gt 1) { + $Route = $pipelineValue + } + + $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag + # override status code with default + if ($Default) { + $code = 'default' + } + else { + $code = "$($StatusCode)" + } + + # add the respones to the routes + foreach ($r in @($Route)) { + foreach ($tag in $DefinitionTag) { + if (! $r.OpenApi.Responses.$tag) { + $r.OpenApi.Responses.$tag = [ordered]@{} + } + $r.OpenApi.Responses.$tag[$code] = New-PodeOResponseInternal -DefinitionTag $tag -Params $PSBoundParameters } - $r.OpenApi.Responses.$tag[$code] = New-PodeOResponseInternal -DefinitionTag $tag -Params $PSBoundParameters } - } - if ($PassThru) { - return $Route + if ($PassThru) { + return $Route + } } - } @@ -685,23 +712,37 @@ function Remove-PodeOAResponse { [switch] $PassThru ) + begin { + # Initialize an array to hold piped-in values + $pipelineValue = @() + } - if ($null -eq $Route) { throw 'The parameter -Route cannot be NULL.' } - - # override status code with default - $code = "$($StatusCode)" - if ($Default) { - $code = 'default' + process { + # Add the current piped-in value to the array + $pipelineValue += $_ } - # remove the respones from the routes - foreach ($r in @($Route)) { - if ($r.OpenApi.Responses.ContainsKey($code)) { - $null = $r.OpenApi.Responses.Remove($code) + + end { + # Set Route to the array of values + if ($pipelineValue.Count -gt 1) { + $Route = $pipelineValue } - } - if ($PassThru) { - return $Route + # override status code with default + $code = "$($StatusCode)" + if ($Default) { + $code = 'default' + } + # remove the respones from the routes + foreach ($r in $Route) { + if ($r.OpenApi.Responses.ContainsKey($code)) { + $null = $r.OpenApi.Responses.Remove($code) + } + } + + if ($PassThru) { + return $Route + } } } @@ -732,7 +773,7 @@ function Set-PodeOARequest { [CmdletBinding()] [OutputType([hashtable[]])] param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [ValidateNotNullOrEmpty()] [hashtable[]] $Route, @@ -746,23 +787,42 @@ function Set-PodeOARequest { [switch] $PassThru ) + begin { + # Initialize an array to hold piped-in values + $pipelineValue = @() + } - if ($null -eq $Route) { throw 'Set-PodeOARequest - The parameter -Route cannot be NULL.' } - - foreach ($r in @($Route)) { + process { + # Add the current piped-in value to the array + $pipelineValue += $_ + } - if (($null -ne $Parameters) -and ($Parameters.Length -gt 0)) { - $r.OpenApi.Parameters = @($Parameters) + end { + # Set Route to the array of values + if ($pipelineValue.Count -gt 1) { + $Route = $pipelineValue } - if ($null -ne $RequestBody) { - $r.OpenApi.RequestBody = $RequestBody - } + foreach ($r in $Route) { - } + if (($null -ne $Parameters) -and ($Parameters.Length -gt 0)) { + $r.OpenApi.Parameters = @($Parameters) + } - if ($PassThru) { - return $Route + if ($null -ne $RequestBody) { + # Only 'POST', 'PUT', 'PATCH' can have a request body + if (('POST', 'PUT', 'PATCH') -inotcontains $r.Method ) { + # {0} operations cannot have a Request Body. + throw ($PodeLocale.getRequestBodyNotAllowedExceptionMessage -f $r.Method) + } + $r.OpenApi.RequestBody = $RequestBody + } + + } + + if ($PassThru) { + return $Route + } } } @@ -832,6 +892,7 @@ New-PodeOARequestBody -Content @{'multipart/form-data' = function New-PodeOARequestBody { [CmdletBinding(DefaultParameterSetName = 'BuiltIn' )] [OutputType([hashtable])] + [OutputType([System.Collections.Specialized.OrderedDictionary])] param( [Parameter(Mandatory = $true, ParameterSetName = 'Reference')] [string] @@ -867,14 +928,11 @@ function New-PodeOARequestBody { $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag - if ($Example -and $Examples) { - throw 'Parameter -Examples and -Example are mutually exclusive' - } - $result = @{} + $result = [ordered]@{} foreach ($tag in $DefinitionTag) { switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { 'builtin' { - $param = @{content = ConvertTo-PodeOAObjectSchema -DefinitionTag $tag -Content $Content -Properties:$Properties } + $param = [ordered]@{content = ConvertTo-PodeOAObjectSchema -DefinitionTag $tag -Content $Content -Properties:$Properties } if ($Required.IsPresent) { $param['required'] = $Required.IsPresent @@ -889,27 +947,27 @@ function New-PodeOARequestBody { $Examples.Remove('*/*') } foreach ($k in $Examples.Keys ) { - if (!$param.content.ContainsKey($k)) { - $param.content[$k] = @{} + if (!($param.content.Keys -contains $k)) { + $param.content[$k] = [ordered]@{} } - $param.content.$k.examples = $Examples.$k + $param.content[$k].examples = $Examples.$k } } } 'reference' { Test-PodeOAComponentInternal -Field requestBodies -DefinitionTag $tag -Name $Reference -PostValidation - $param = @{ + $param = [ordered]@{ '$ref' = "#/components/requestBodies/$Reference" } } } if ($Encoding) { if (([string]$Content.keys[0]) -match '(?i)^(multipart.*|application\/x-www-form-urlencoded)$' ) { - $r = @{} + $r = [ordered]@{} foreach ( $e in $Encoding) { $key = [string]$e.Keys - $elems = @{} + $elems = [ordered]@{} foreach ($v in $e[$key].Keys) { if ($v -ieq 'headers') { $elems.headers = ConvertTo-PodeOAHeaderProperty -Headers $e[$key].headers @@ -923,7 +981,8 @@ function New-PodeOARequestBody { $param.Content.$($Content.keys[0]).encoding = $r } else { - throw 'The encoding attribute is only applicable to multipart and application/x-www-form-urlencoded request bodies.' + # The encoding attribute only applies to multipart and application/x-www-form-urlencoded request bodies + throw ($PodeLocale.encodingAttributeOnlyAppliesToMultipartExceptionMessage) } } $result[$tag] = $param @@ -974,11 +1033,17 @@ function Test-PodeOAJsonSchemaCompliance { ) if ($DefinitionTag) { if (! ($PodeContext.Server.OpenApi.Definitions.Keys -ccontains $DefinitionTag)) { - throw "DefinitionTag $DefinitionTag is not defined" + # DefinitionTag does not exist. + throw ($PodeLocale.definitionTagNotDefinedExceptionMessage -f $DefinitionTag) } } else { - $DefinitionTag = $PodeContext.Server.OpenAPI.DefaultDefinitionTag + $DefinitionTag = $PodeContext.Server.Web.OpenApi.DefaultDefinitionTag + } + + # if Powershell edition is Desktop the test cannot be done. By default everything is good + if ($PSVersionTable.PSEdition -eq 'Desktop') { + return $true } if ($Json -isnot [string]) { @@ -986,10 +1051,12 @@ function Test-PodeOAJsonSchemaCompliance { } if (!$PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.schemaValidation) { - throw 'Test-PodeOAComponentchema need to be enabled using `Enable-PodeOpenApi -EnableSchemaValidation` ' + # 'Test-PodeOAComponentchema' need to be enabled using 'Enable-PodeOpenApi -EnableSchemaValidation' + throw ($PodeLocale.testPodeOAComponentSchemaNeedToBeEnabledExceptionMessage) } if (!(Test-PodeOAComponentSchemaJson -Name $SchemaReference -DefinitionTag $DefinitionTag)) { - throw "The OpenApi component schema in Json doesn't exist: $SchemaReference" + # The OpenApi component schema doesn't exist + throw ($PodeLocale.openApiComponentSchemaDoesNotExistExceptionMessage -f $SchemaReference) } if ($PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.schemaJson[$SchemaReference].available) { [string[]] $message = @() @@ -1093,8 +1160,8 @@ function ConvertTo-PodeOAParameter { [string] $In, - [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Properties')] - [Parameter( Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ContentProperties')] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ParameterSetName = 'Properties')] + [Parameter( Mandatory = $true, Position = 0, ValueFromPipeline = $true, ParameterSetName = 'ContentProperties')] [ValidateNotNull()] [hashtable] $Property, @@ -1182,266 +1249,298 @@ function ConvertTo-PodeOAParameter { [string[]] $DefinitionTag ) + begin { + $pipelineItemCount = 0 + } - $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag + process { + $pipelineItemCount++ + } - if ($PSCmdlet.ParameterSetName -ieq 'ContentSchema' -or $PSCmdlet.ParameterSetName -ieq 'Schema') { - if (Test-PodeIsEmpty $Schema) { - return $null - } - Test-PodeOAComponentInternal -Field schemas -DefinitionTag $DefinitionTag -Name $Schema -PostValidation - if (!$Name ) { - $Name = $Schema - } - $prop = [ordered]@{ - in = $In.ToLowerInvariant() - name = $Name - } - if ($In -ieq 'Header' -and $PodeContext.Server.Security.autoHeaders) { - Add-PodeSecurityHeader -Name 'Access-Control-Allow-Headers' -Value $Schema -Append - } - if ($AllowEmptyValue.IsPresent ) { - $prop['allowEmptyValue'] = $AllowEmptyValue.IsPresent - } - if ($Required.IsPresent ) { - $prop['required'] = $Required.IsPresent - } - if ($Description ) { - $prop.description = $Description - } - if ($Deprecated.IsPresent ) { - $prop.deprecated = $Deprecated.IsPresent + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) } - if ($ContentType ) { - # ensure all content types are valid - if ($ContentType -inotmatch '^[\w-]+\/[\w\.\+-]+$') { - throw "Invalid content-type found for schema: $($type)" + + $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag + + if ($PSCmdlet.ParameterSetName -ieq 'ContentSchema' -or $PSCmdlet.ParameterSetName -ieq 'Schema') { + if (Test-PodeIsEmpty $Schema) { + return $null } - $prop.content = [ordered]@{ - $ContentType = [ordered]@{ - schema = [ordered]@{ - '$ref' = "#/components/schemas/$($Schema )" - } - } + Test-PodeOAComponentInternal -Field schemas -DefinitionTag $DefinitionTag -Name $Schema -PostValidation + if (!$Name ) { + $Name = $Schema } - if ($Example ) { - $prop.content.$ContentType.example = $Example + $prop = [ordered]@{ + in = $In.ToLowerInvariant() + name = $Name } - elseif ($Examples) { - $prop.content.$ContentType.examples = $Examples + if ($In -ieq 'Header' -and $PodeContext.Server.Security.autoHeaders) { + Add-PodeSecurityHeader -Name 'Access-Control-Allow-Headers' -Value $Schema -Append } - } - else { - $prop.schema = [ordered]@{ - '$ref' = "#/components/schemas/$($Schema )" - } - if ($Style) { - switch ($in.ToLower()) { - 'path' { - if (@('Simple', 'Label', 'Matrix' ) -inotcontains $Style) { - throw "OpenApi request Style cannot be $Style for a $in parameter" + if ($AllowEmptyValue.IsPresent ) { + $prop['allowEmptyValue'] = $AllowEmptyValue.IsPresent + } + if ($Required.IsPresent ) { + $prop['required'] = $Required.IsPresent + } + if ($Description ) { + $prop.description = $Description + } + if ($Deprecated.IsPresent ) { + $prop.deprecated = $Deprecated.IsPresent + } + if ($ContentType ) { + # ensure all content types are valid + if ($ContentType -inotmatch '^[\w-]+\/[\w\.\+-]+$') { + # Invalid 'content-type' found for schema: $type + throw ($PodeLocale.invalidContentTypeForSchemaExceptionMessage -f $type) + } + $prop.content = [ordered]@{ + $ContentType = [ordered]@{ + schema = [ordered]@{ + '$ref' = "#/components/schemas/$($Schema )" } - break } - 'query' { - if (@('Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject' ) -inotcontains $Style) { - throw "OpenApi request Style cannot be $Style for a $in parameter" + } + if ($Example ) { + $prop.content.$ContentType.example = $Example + } + elseif ($Examples) { + $prop.content.$ContentType.examples = $Examples + } + } + else { + $prop.schema = [ordered]@{ + '$ref' = "#/components/schemas/$($Schema )" + } + if ($Style) { + switch ($in.ToLower()) { + 'path' { + if (@('Simple', 'Label', 'Matrix' ) -inotcontains $Style) { + # OpenApi request Style cannot be $Style for a $in parameter + throw ($PodeLocale.openApiRequestStyleInvalidForParameterExceptionMessage -f $Style, $in) + } + break } - break - } - 'header' { - if (@('Simple' ) -inotcontains $Style) { - throw "OpenApi request Style cannot be $Style for a $in parameter" + 'query' { + if (@('Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject' ) -inotcontains $Style) { + # OpenApi request Style cannot be $Style for a $in parameter + throw ($PodeLocale.openApiRequestStyleInvalidForParameterExceptionMessage -f $Style, $in) + } + break } - break - } - 'cookie' { - if (@('Form' ) -inotcontains $Style) { - throw "OpenApi request Style cannot be $Style for a $in parameter" + 'header' { + if (@('Simple' ) -inotcontains $Style) { + # OpenApi request Style cannot be $Style for a $in parameter + throw ($PodeLocale.openApiRequestStyleInvalidForParameterExceptionMessage -f $Style, $in) + } + break + } + 'cookie' { + if (@('Form' ) -inotcontains $Style) { + # OpenApi request Style cannot be $Style for a $in parameter + throw ($PodeLocale.openApiRequestStyleInvalidForParameterExceptionMessage -f $Style, $in) + } + break } - break } + $prop['style'] = $Style.Substring(0, 1).ToLower() + $Style.Substring(1) } - $prop['style'] = $Style.Substring(0, 1).ToLower() + $Style.Substring(1) - } - if ($Explode.IsPresent ) { - $prop['explode'] = $Explode.IsPresent - } + if ($Explode.IsPresent ) { + $prop['explode'] = $Explode.IsPresent + } - if ($AllowEmptyValue.IsPresent ) { - $prop['allowEmptyValue'] = $AllowEmptyValue.IsPresent - } + if ($AllowEmptyValue.IsPresent ) { + $prop['allowEmptyValue'] = $AllowEmptyValue.IsPresent + } - if ($AllowReserved.IsPresent) { - $prop['allowReserved'] = $AllowReserved.IsPresent - } + if ($AllowReserved.IsPresent) { + $prop['allowReserved'] = $AllowReserved.IsPresent + } - } - } - elseif ($PSCmdlet.ParameterSetName -ieq 'Reference') { - # return a reference - Test-PodeOAComponentInternal -Field parameters -DefinitionTag $DefinitionTag -Name $Reference -PostValidation - $prop = [ordered]@{ - '$ref' = "#/components/parameters/$Reference" - } - foreach ($tag in $DefinitionTag) { - if ($PodeContext.Server.OpenAPI.Definitions[$tag].components.parameters.$Reference.In -eq 'Header' -and $PodeContext.Server.Security.autoHeaders) { - Add-PodeSecurityHeader -Name 'Access-Control-Allow-Headers' -Value $Reference -Append + if ($Example ) { + $prop.example = $Example + } + elseif ($Examples) { + $prop.examples = $Examples + } } } - } - else { - - if (!$Name ) { - if ($Property.name) { - $Name = $Property.name + elseif ($PSCmdlet.ParameterSetName -ieq 'Reference') { + # return a reference + Test-PodeOAComponentInternal -Field parameters -DefinitionTag $DefinitionTag -Name $Reference -PostValidation + $prop = [ordered]@{ + '$ref' = "#/components/parameters/$Reference" } - else { - throw 'Parameter requires a Name' + foreach ($tag in $DefinitionTag) { + if ($PodeContext.Server.OpenAPI.Definitions[$tag].components.parameters.$Reference.In -eq 'Header' -and $PodeContext.Server.Security.autoHeaders) { + Add-PodeSecurityHeader -Name 'Access-Control-Allow-Headers' -Value $Reference -Append + } } } - if ($In -ieq 'Header' -and $PodeContext.Server.Security.autoHeaders -and $Name ) { - Add-PodeSecurityHeader -Name 'Access-Control-Allow-Headers' -Value $Name -Append - } + else { - # build the base parameter - $prop = [ordered]@{ - in = $In.ToLowerInvariant() - name = $Name - } - $sch = [ordered]@{} - if ($Property.array) { - $sch.type = 'array' - $sch.items = [ordered]@{ - type = $Property.type + if (!$Name ) { + if ($Property.name) { + $Name = $Property.name + } + else { + # The OpenApi parameter requires a name to be specified + throw ($PodeLocale.openApiParameterRequiresNameExceptionMessage) + } } - if ($Property.format) { - $sch.items.format = $Property.format + if ($In -ieq 'Header' -and $PodeContext.Server.Security.autoHeaders -and $Name ) { + Add-PodeSecurityHeader -Name 'Access-Control-Allow-Headers' -Value $Name -Append } - } - else { - $sch.type = $Property.type - if ($Property.format) { - $sch.format = $Property.format + + # build the base parameter + $prop = [ordered]@{ + in = $In.ToLowerInvariant() + name = $Name } - } - if ($ContentType) { - if ($ContentType -inotmatch '^[\w-]+\/[\w\.\+-]+$') { - throw "Invalid content-type found for schema: $($type)" + $sch = [ordered]@{} + if ($Property.array) { + $sch.type = 'array' + $sch.items = [ordered]@{ + type = $Property.type + } + if ($Property.format) { + $sch.items.format = $Property.format + } } - $prop.content = [ordered]@{ - $ContentType = [ordered] @{ - schema = $sch + else { + $sch.type = $Property.type + if ($Property.format) { + $sch.format = $Property.format } } - } - else { - $prop.schema = $sch - } + if ($ContentType) { + if ($ContentType -inotmatch '^[\w-]+\/[\w\.\+-]+$') { + # Invalid 'content-type' found for schema: $type + throw ($PodeLocale.invalidContentTypeForSchemaExceptionMessage -f $type) + } + $prop.content = [ordered]@{ + $ContentType = [ordered] @{ + schema = $sch + } + } + } + else { + $prop.schema = $sch + } - if ($Example -and $Examples) { - throw '-Example and -Examples are mutually exclusive' - } - if ($AllowEmptyValue.IsPresent ) { - $prop['allowEmptyValue'] = $AllowEmptyValue.IsPresent - } + if ($Example -and $Examples) { + # Parameters 'Examples' and 'Example' are mutually exclusive + throw ($PodeLocale.parametersMutuallyExclusiveExceptionMessage -f 'Examples' , 'Example' ) + } + if ($AllowEmptyValue.IsPresent ) { + $prop['allowEmptyValue'] = $AllowEmptyValue.IsPresent + } - if ($Description ) { - $prop.description = $Description - } - elseif ($Property.description) { - $prop.description = $Property.description - } + if ($Description ) { + $prop.description = $Description + } + elseif ($Property.description) { + $prop.description = $Property.description + } - if ($Required.IsPresent ) { - $prop.required = $Required.IsPresent - } - elseif ($Property.required) { - $prop.required = $Property.required - } + if ($Required.IsPresent ) { + $prop.required = $Required.IsPresent + } + elseif ($Property.required) { + $prop.required = $Property.required + } - if ($Deprecated.IsPresent ) { - $prop.deprecated = $Deprecated.IsPresent - } - elseif ($Property.deprecated) { - $prop.deprecated = $Property.deprecated - } + if ($Deprecated.IsPresent ) { + $prop.deprecated = $Deprecated.IsPresent + } + elseif ($Property.deprecated) { + $prop.deprecated = $Property.deprecated + } - if (!$ContentType) { - if ($Style) { - switch ($in.ToLower()) { - 'path' { - if (@('Simple', 'Label', 'Matrix' ) -inotcontains $Style) { - throw "OpenApi request Style cannot be $Style for a $in parameter" + if (!$ContentType) { + if ($Style) { + switch ($in.ToLower()) { + 'path' { + if (@('Simple', 'Label', 'Matrix' ) -inotcontains $Style) { + # OpenApi request Style cannot be $Style for a $in parameter + throw ($PodeLocale.openApiRequestStyleInvalidForParameterExceptionMessage -f $Style, $in) + } + break } - break - } - 'query' { - if (@('Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject' ) -inotcontains $Style) { - throw "OpenApi request Style cannot be $Style for a $in parameter" + 'query' { + if (@('Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject' ) -inotcontains $Style) { + # OpenApi request Style cannot be $Style for a $in parameter + throw ($PodeLocale.openApiRequestStyleInvalidForParameterExceptionMessage -f $Style, $in) + } + break } - break - } - 'header' { - if (@('Simple' ) -inotcontains $Style) { - throw "OpenApi request Style cannot be $Style for a $in parameter" + 'header' { + if (@('Simple' ) -inotcontains $Style) { + # OpenApi request Style cannot be $Style for a $in parameter + throw ($PodeLocale.openApiRequestStyleInvalidForParameterExceptionMessage -f $Style, $in) + } + break } - break - } - 'cookie' { - if (@('Form' ) -inotcontains $Style) { - throw "OpenApi request Style cannot be $Style for a $in parameter" + 'cookie' { + if (@('Form' ) -inotcontains $Style) { + # OpenApi request Style cannot be $Style for a $in parameter + throw ($PodeLocale.openApiRequestStyleInvalidForParameterExceptionMessage -f $Style, $in) + } + break } - break } + $prop['style'] = $Style.Substring(0, 1).ToLower() + $Style.Substring(1) } - $prop['style'] = $Style.Substring(0, 1).ToLower() + $Style.Substring(1) - } - if ($Explode.IsPresent ) { - $prop['explode'] = $Explode.IsPresent - } + if ($Explode.IsPresent ) { + $prop['explode'] = $Explode.IsPresent + } - if ($AllowReserved.IsPresent) { - $prop['allowReserved'] = $AllowReserved.IsPresent - } + if ($AllowReserved.IsPresent) { + $prop['allowReserved'] = $AllowReserved.IsPresent + } - if ($Example ) { - $prop['example'] = $Example - } - elseif ($Examples) { - $prop['examples'] = $Examples - } + if ($Example ) { + $prop['example'] = $Example + } + elseif ($Examples) { + $prop['examples'] = $Examples + } - if ($Property.default -and !$prop.required ) { - $prop.schema['default'] = $Property.default - } + if ($Property.default -and !$prop.required ) { + $prop.schema['default'] = $Property.default + } - if ($Property.enum) { - if ($Property.array) { - $prop.schema.items['enum'] = $Property.enum + if ($Property.enum) { + if ($Property.array) { + $prop.schema.items['enum'] = $Property.enum + } + else { + $prop.schema['enum'] = $Property.enum + } } - else { - $prop.schema['enum'] = $Property.enum + } + else { + if ($Example ) { + $prop.content.$ContentType.example = $Example + } + elseif ($Examples) { + $prop.content.$ContentType.examples = $Examples } } } - else { - if ($Example ) { - $prop.content.$ContentType.example = $Example - } - elseif ($Examples) { - $prop.content.$ContentType.examples = $Examples - } + + if ($In -ieq 'Path' -and !$prop.required ) { + # If the parameter location is 'Path', the switch parameter 'Required' is mandatory + throw ($PodeLocale.pathParameterRequiresRequiredSwitchExceptionMessage) } - } - if ($In -ieq 'Path' -and !$prop.required ) { - Throw "If the parameter location is 'Path', the switch parameter `-Required` is required" + return $prop } - - return $prop } <# @@ -1488,7 +1587,7 @@ function Set-PodeOARouteInfo { [CmdletBinding()] [OutputType([hashtable[]])] param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [ValidateNotNullOrEmpty()] [hashtable[]] $Route, @@ -1517,49 +1616,77 @@ function Set-PodeOARouteInfo { [string[]] $DefinitionTag ) + begin { + # Initialize an array to hold piped-in values + $pipelineValue = @() + } - if ($null -eq $Route) { throw 'Set-PodeOARouteInfo - The parameter -Route cannot be NULL.' } - - $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag + process { + # Add the current piped-in value to the array + $pipelineValue += $_ + } - foreach ($r in @($Route)) { + end { + # Set Route to the array of values + if ($pipelineValue.Count -gt 1) { + $Route = $pipelineValue + } - $r.OpenApi.DefinitionTag = $DefinitionTag + $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag - if ($Summary) { - $r.OpenApi.Summary = $Summary - } - if ($Description) { - $r.OpenApi.Description = $Description - } - if ($OperationId) { - if ($Route.Count -gt 1) { - throw "OperationID:$OperationId has to be unique and cannot be applied to an array." + foreach ($r in @($Route)) { + if ((Compare-Object -ReferenceObject $r.OpenApi.DefinitionTag -DifferenceObject $DefinitionTag).Count -ne 0) { + if ($r.OpenApi.IsDefTagConfigured ) { + # Definition Tag for a Route cannot be changed. + throw ($PodeLocale.definitionTagChangeNotAllowedExceptionMessage) + } + else { + $r.OpenApi.DefinitionTag = $DefinitionTag + $r.OpenApi.IsDefTagConfigured = $true + } } - foreach ($tag in $DefinitionTag) { - if ($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId -ccontains $OperationId) { - throw "OperationID:$OperationId has to be unique." + + if ($OperationId) { + if ($Route.Count -gt 1) { + # OperationID:$OperationId has to be unique and cannot be applied to an array + throw ($PodeLocale.operationIdMustBeUniqueForArrayExceptionMessage -f $OperationId) } - $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId += $OperationId + foreach ($tag in $DefinitionTag) { + if ($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId -ccontains $OperationId) { + # OperationID:$OperationId has to be unique + throw ($PodeLocale.operationIdMustBeUniqueExceptionMessage -f $OperationId) + } + $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId += $OperationId + } + $r.OpenApi.OperationId = $OperationId } - $r.OpenApi.OperationId = $OperationId - } - if ($Tags) { - $r.OpenApi.Tags = $Tags - } - if ($ExternalDocs) { - $r.OpenApi.ExternalDocs = $ExternalDoc - } + if ($Summary) { + $r.OpenApi.Summary = $Summary + } + + if ($Description) { + $r.OpenApi.Description = $Description + } + + if ($Tags) { + $r.OpenApi.Tags = $Tags + } - $r.OpenApi.Swagger = $true - if ($Deprecated.IsPresent) { - $r.OpenApi.Deprecated = $Deprecated.IsPresent + if ($ExternalDocs) { + $r.OpenApi.ExternalDocs = $ExternalDoc + } + + $r.OpenApi.Swagger = $true + + if ($Deprecated.IsPresent) { + $r.OpenApi.Deprecated = $Deprecated.IsPresent + } } - } - if ($PassThru) { - return $Route + if ($PassThru) { + return $Route + } } } @@ -1607,8 +1734,8 @@ The title of the web page. (Default is the OpenAPI title from Enable-PodeOpenApi If supplied, the page will be rendered using a dark theme (this is not supported for all viewers). .PARAMETER EndpointName -The EndpointName of an Endpoint(s) to bind the static Route against. - +The EndpointName of an Endpoint(s) to bind the static Route against.This parameter is normally not required. +The Endpoint is retrieved by the OpenAPI DefinitionTag .PARAMETER Authentication The name of an Authentication method which should be used as middleware on this Route. @@ -1705,26 +1832,37 @@ function Enable-PodeOAViewer { $DefinitionTag ) $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag + + # If no EndpointName try to reetrieve the EndpointName from the DefinitionTag if exist + if ([string]::IsNullOrWhiteSpace($EndpointName) -and $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.EndpointName) { + $EndpointName = $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.EndpointName + } + # error if there's no OpenAPI URL $OpenApiUrl = Protect-PodeValue -Value $OpenApiUrl -Default $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].Path if ([string]::IsNullOrWhiteSpace($OpenApiUrl)) { - throw "No OpenAPI URL supplied for $($Type)" + # No OpenAPI URL supplied for $Type + throw ($PodeLocale.noOpenApiUrlSuppliedExceptionMessage -f $Type) + } # fail if no title $Title = Protect-PodeValue -Value $Title -Default $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].info.Title if ([string]::IsNullOrWhiteSpace($Title)) { - throw "No title supplied for $($Type) page" + # No title supplied for $Type page + throw ($PodeLocale.noTitleSuppliedForPageExceptionMessage -f $Type) } if ($Editor.IsPresent) { # set a default path $Path = Protect-PodeValue -Value $Path -Default '/editor' if ([string]::IsNullOrWhiteSpace($Title)) { - throw "No route path supplied for $($Type) page" + # No route path supplied for $Type page + throw ($PodeLocale.noRoutePathSuppliedForPageExceptionMessage -f $Type) } if (Test-PodeOAVersion -Version 3.1 -DefinitionTag $DefinitionTag) { - throw "This version on Swagger-Editor doesn't support OpenAPI 3.1" + # This version on Swagger-Editor doesn't support OpenAPI 3.1 + throw ($PodeLocale.swaggerEditorDoesNotSupportOpenApi31ExceptionMessage) } # setup meta info $meta = @{ @@ -1756,7 +1894,8 @@ function Enable-PodeOAViewer { # set a default path $Path = Protect-PodeValue -Value $Path -Default '/bookmarks' if ([string]::IsNullOrWhiteSpace($Title)) { - throw "No route path supplied for $($Type) page" + # No route path supplied for $Type page + throw ($PodeLocale.noRoutePathSuppliedForPageExceptionMessage -f $Type) } # setup meta info $meta = @{ @@ -1797,12 +1936,14 @@ function Enable-PodeOAViewer { } else { if ($Type -ieq 'RapiPdf' -and (Test-PodeOAVersion -Version 3.1 -DefinitionTag $DefinitionTag)) { - throw "The Document tool RapidPdf doesn't support OpenAPI 3.1" + # The Document tool RapidPdf doesn't support OpenAPI 3.1 + throw ($PodeLocale.rapidPdfDoesNotSupportOpenApi31ExceptionMessage) } # set a default path $Path = Protect-PodeValue -Value $Path -Default "/$($Type.ToLowerInvariant())" if ([string]::IsNullOrWhiteSpace($Title)) { - throw "No route path supplied for $($Type) page" + # No route path supplied for $Type page + throw ($PodeLocale.noRoutePathSuppliedForPageExceptionMessage -f $Type) } # setup meta info $meta = @{ @@ -1910,7 +2051,7 @@ $ExtDoc|Add-PodeOAExternalDoc function Add-PodeOAExternalDoc { [CmdletBinding(DefaultParameterSetName = 'Pipe')] param( - [Parameter(ValueFromPipeline = $true, DontShow = $true, ParameterSetName = 'Pipe')] + [Parameter(ValueFromPipeline = $true, Position = 0, DontShow = $true, ParameterSetName = 'Pipe')] [System.Collections.Specialized.OrderedDictionary ] $ExternalDoc, @@ -1925,22 +2066,33 @@ function Add-PodeOAExternalDoc { [string[]] $DefinitionTag ) + begin { + $pipelineItemCount = 0 + } - $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag + process { + $pipelineItemCount++ + } - foreach ($tag in $DefinitionTag) { - if ($PSCmdlet.ParameterSetName -ieq 'NewRef') { - $param = [ordered]@{url = $Url } - if ($Description) { - $param.description = $Description - } - $PodeContext.Server.OpenAPI.Definitions[$tag].externalDocs = $param + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) } - else { - $PodeContext.Server.OpenAPI.Definitions[$tag].externalDocs = $ExternalDoc + $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag + + foreach ($tag in $DefinitionTag) { + if ($PSCmdlet.ParameterSetName -ieq 'NewRef') { + $param = [ordered]@{url = $Url } + if ($Description) { + $param.description = $Description + } + $PodeContext.Server.OpenAPI.Definitions[$tag].externalDocs = $param + } + else { + $PodeContext.Server.OpenAPI.Definitions[$tag].externalDocs = $ExternalDoc + } } } - } @@ -2106,7 +2258,8 @@ function Add-PodeOAInfo { $Info.license.url = $LicenseUrl } else { - throw 'The OpenAPI property license.name is required. Use -LicenseName' + # The OpenAPI object 'license' required the property 'name'. Use -LicenseName parameter. + throw ($PodeLocale.openApiLicenseObjectRequiresNameExceptionMessage) } } @@ -2161,63 +2314,65 @@ function Add-PodeOAInfo { <# .SYNOPSIS -Creates a new OpenAPI example. + Creates a new OpenAPI example. .DESCRIPTION -Creates a new OpenAPI example. + Creates a new OpenAPI example. + + .PARAMETER ParamsList + Used to pipeline multiple properties -.PARAMETER ParamsList -Used to pipeline multiple properties +.PARAMETER ContentType + The Media Content Type associated with the Example. -.PARAMETER MediaType -The Media Type associated with the Example. + Alias: MediaType .PARAMETER Name -The Name of the Example. + The Name of the Example. .PARAMETER Summary -Short description for the example - + Short description for the example -.PARAMETER Description -Long description for the example. + .PARAMETER Description + Long description for the example. .PARAMETER Reference -A reference to a reusable component example + A reference to a reusable component example .PARAMETER Value -Embedded literal example. The value Parameter and ExternalValue parameter are mutually exclusive. -To represent examples of media types that cannot naturally represented in JSON or YAML, use a string value to contain the example, escaping where necessary. + Embedded literal example. The value Parameter and ExternalValue parameter are mutually exclusive. + To represent examples of media types that cannot naturally represented in JSON or YAML, use a string value to contain the example, escaping where necessary. .PARAMETER ExternalValue -A URL that points to the literal example. This provides the capability to reference examples that cannot easily be included in JSON or YAML documents. -The -Value parameter and -ExternalValue parameter are mutually exclusive. | + A URL that points to the literal example. This provides the capability to reference examples that cannot easily be included in JSON or YAML documents. + The -Value parameter and -ExternalValue parameter are mutually exclusive. | .PARAMETER DefinitionTag -An Array of strings representing the unique tag for the API specification. -This tag helps distinguish between different versions or types of API specifications within the application. -You can use this tag to reference the specific API documentation, schema, or version that your function interacts with. + An Array of strings representing the unique tag for the API specification. + This tag helps distinguish between different versions or types of API specifications within the application. + You can use this tag to reference the specific API documentation, schema, or version that your function interacts with. .EXAMPLE -New-PodeOAExample -ContentMediaType 'text/plain' -Name 'user' -Summary = 'User Example in Plain text' -ExternalValue = 'http://foo.bar/examples/user-example.txt' + New-PodeOAExample -ContentType 'text/plain' -Name 'user' -Summary = 'User Example in Plain text' -ExternalValue = 'http://foo.bar/examples/user-example.txt' .EXAMPLE -$example = - New-PodeOAExample -ContentMediaType 'application/json' -Name 'user' -Summary = 'User Example' -ExternalValue = 'http://foo.bar/examples/user-example.json' | - New-PodeOAExample -ContentMediaType 'application/xml' -Name 'user' -Summary = 'User Example in XML' -ExternalValue = 'http://foo.bar/examples/user-example.xml' - + $example = + New-PodeOAExample -ContentType 'application/json' -Name 'user' -Summary = 'User Example' -ExternalValue = 'http://foo.bar/examples/user-example.json' | + New-PodeOAExample -ContentType 'application/xml' -Name 'user' -Summary = 'User Example in XML' -ExternalValue = 'http://foo.bar/examples/user-example.xml' #> function New-PodeOAExample { [CmdletBinding(DefaultParameterSetName = 'Inbuilt')] [OutputType([System.Collections.Specialized.OrderedDictionary ])] param( - [Parameter(ValueFromPipeline = $true, DontShow = $true, ParameterSetName = 'Inbuilt')] - [Parameter(ValueFromPipeline = $true, DontShow = $true, ParameterSetName = 'Reference')] + [Parameter(ValueFromPipeline = $true, Position = 0, DontShow = $true, ParameterSetName = 'Inbuilt')] + [Parameter(ValueFromPipeline = $true, Position = 0, DontShow = $true, ParameterSetName = 'Reference')] [System.Collections.Specialized.OrderedDictionary ] $ParamsList, + [Parameter()] + [Alias('MediaType')] [string] - $MediaType, + $ContentType, [Parameter(Mandatory = $true, ParameterSetName = 'Inbuilt')] [ValidatePattern('^[a-zA-Z0-9\.\-_]+$')] @@ -2249,11 +2404,11 @@ function New-PodeOAExample { $DefinitionTag ) begin { + $pipelineValue = [ordered]@{} if (Test-PodeIsEmpty -Value $DefinitionTag) { $DefinitionTag = $PodeContext.Server.OpenAPI.SelectedDefinitionTag } - if ($PSCmdlet.ParameterSetName -ieq 'Reference') { Test-PodeOAComponentInternal -Field examples -DefinitionTag $DefinitionTag -Name $Reference -PostValidation $Name = $Reference @@ -2261,7 +2416,8 @@ function New-PodeOAExample { } else { if ( $ExternalValue -and $Value) { - throw '-Value or -ExternalValue are mutually exclusive' + # Parameters 'ExternalValue' and 'Value' are mutually exclusive + throw ($PodeLocale.parametersMutuallyExclusiveExceptionMessage -f 'ExternalValue', 'Value') } $Example = [ordered]@{ } if ($Summary) { @@ -2277,34 +2433,45 @@ function New-PodeOAExample { $Example.externalValue = $ExternalValue } else { - throw '-Value or -ExternalValue are mandatory' + # Parameters 'Value' or 'ExternalValue' are mandatory + throw ($PodeLocale.parametersValueOrExternalValueMandatoryExceptionMessage) } } $param = [ordered]@{} - if ($MediaType) { - $param.$MediaType = [ordered]@{ + if ($ContentType) { + $param.$ContentType = [ordered]@{ $Name = $Example } } else { $param.$Name = $Example } + } process { + if ($_) { + $pipelineValue += $_ + } } end { - if ($ParamsList) { - if ($ParamsList.keys -contains $param.Keys[0]) { - $param.Values[0].GetEnumerator() | ForEach-Object { $ParamsList[$param.Keys[0]].$($_.Key) = $_.Value } - } - else { - $param.GetEnumerator() | ForEach-Object { $ParamsList[$_.Key] = $_.Value } - } - return $ParamsList + $examples = [ordered]@{} + if ($pipelineValue.Count -gt 0) { + # foreach ($p in $pipelineValue) { + $examples = $pipelineValue + # } + } + else { + return $param + } + + $key = [string]$param.Keys[0] + if ($examples.Keys -contains $key) { + $examples[$key] += $param[$key] } else { - return [System.Collections.Specialized.OrderedDictionary] $param + $examples += $param } + return $examples } } @@ -2353,7 +2520,7 @@ New-PodeOAEncodingObject -Name 'profileImage' -ContentType 'image/png, image/jpe #> function New-PodeOAEncodingObject { param ( - [Parameter(ValueFromPipeline = $true, DontShow = $true )] + [Parameter(ValueFromPipeline = $true, Position = 0, DontShow = $true )] [hashtable[]] $EncodingList, @@ -2470,7 +2637,7 @@ If supplied, the route passed in will be returned for further chaining. Add-PodeOACallBack -Title 'test' -Path '{$request.body#/id}' -Method Post ` -RequestBody (New-PodeOARequestBody -Content @{'*/*' = (New-PodeOAStringProperty -Name 'id')}) ` -Response ( - New-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentMediaType 'application/json','application/xml' -Content 'Pet' -Array) + New-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json','application/xml' -Content 'Pet' -Array) New-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' | New-PodeOAResponse -StatusCode 404 -Description 'Pet not found' | New-PodeOAResponse -Default -Description 'Something is wrong' @@ -2486,7 +2653,7 @@ function Add-PodeOACallBack { [CmdletBinding(DefaultParameterSetName = 'inbuilt')] [OutputType([hashtable[]])] param ( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [ValidateNotNullOrEmpty()] [hashtable[]] $Route, @@ -2528,36 +2695,50 @@ function Add-PodeOACallBack { [string[]] $DefinitionTag ) + begin { + # Initialize an array to hold piped-in values + $pipelineValue = @() + } - if ($null -eq $Route) { throw 'Add-PodeOACallBack - The parameter -Route cannot be NULL.' } + process { + # Add the current piped-in value to the array + $pipelineValue += $_ + } - $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag + end { + # Set Route to the array of values + if ($pipelineValue.Count -gt 1) { + $Route = $pipelineValue + } - foreach ($r in @($Route)) { - foreach ($tag in $DefinitionTag) { - if ($Reference) { - Test-PodeOAComponentInternal -Field callbacks -DefinitionTag $tag -Name $Reference -PostValidation - if (!$Name) { - $Name = $Reference - } - if (! $r.OpenApi.CallBacks.ContainsKey($tag)) { - $r.OpenApi.CallBacks[$tag] = [ordered]@{} - } - $r.OpenApi.CallBacks[$tag].$Name = @{ - '$ref' = "#/components/callbacks/$Reference" + $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag + + foreach ($r in @($Route)) { + foreach ($tag in $DefinitionTag) { + if ($Reference) { + Test-PodeOAComponentInternal -Field callbacks -DefinitionTag $tag -Name $Reference -PostValidation + if (!$Name) { + $Name = $Reference + } + if (! $r.OpenApi.CallBacks.ContainsKey($tag)) { + $r.OpenApi.CallBacks[$tag] = [ordered]@{} + } + $r.OpenApi.CallBacks[$tag].$Name = [ordered]@{ + '$ref' = "#/components/callbacks/$Reference" + } } - } - else { - if (! $r.OpenApi.CallBacks.ContainsKey($tag)) { - $r.OpenApi.CallBacks[$tag] = [ordered]@{} + else { + if (! $r.OpenApi.CallBacks.ContainsKey($tag)) { + $r.OpenApi.CallBacks[$tag] = [ordered]@{} + } + $r.OpenApi.CallBacks[$tag].$Name = New-PodeOAComponentCallBackInternal -Params $PSBoundParameters -DefinitionTag $tag } - $r.OpenApi.CallBacks[$tag].$Name = New-PodeOAComponentCallBackInternal -Params $PSBoundParameters -DefinitionTag $tag } } - } - if ($PassThru) { - return $Route + if ($PassThru) { + return $Route + } } } @@ -2602,7 +2783,7 @@ This tag helps distinguish between different versions or types of API specificat You can use this tag to reference the specific API documentation, schema, or version that your function interacts with. .EXAMPLE -New-PodeOAResponse -StatusCode 200 -Content ( New-PodeOAContentMediaType -ContentMediaType 'application/json' -Content(New-PodeOAIntProperty -Name 'userId' -Object) ) +New-PodeOAResponse -StatusCode 200 -Content ( New-PodeOAContentMediaType -ContentType 'application/json' -Content(New-PodeOAIntProperty -Name 'userId' -Object) ) .EXAMPLE New-PodeOAResponse -StatusCode 200 -Content @{ 'application/json' = 'UserIdSchema' } @@ -2612,10 +2793,10 @@ New-PodeOAResponse -StatusCode 200 -Reference 'OKResponse' .EXAMPLE Add-PodeOACallBack -Title 'test' -Path '$request.body#/id' -Method Post -RequestBody ( - New-PodeOARequestBody -Content (New-PodeOAContentMediaType -ContentMediaType '*/*' -Content (New-PodeOAStringProperty -Name 'id')) + New-PodeOARequestBody -Content (New-PodeOAContentMediaType -ContentType '*/*' -Content (New-PodeOAStringProperty -Name 'id')) ) ` -Response ( - New-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentMediaType 'application/json','application/xml' -Content 'Pet' -Array) | + New-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json','application/xml' -Content 'Pet' -Array) | New-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' | New-PodeOAResponse -StatusCode 404 -Description 'Pet not found' | New-PodeOAResponse -Default -Description 'Something is wrong' @@ -2626,7 +2807,7 @@ function New-PodeOAResponse { [CmdletBinding(DefaultParameterSetName = 'Schema')] [OutputType([hashtable])] param( - [Parameter(ValueFromPipeline = $true , DontShow = $true )] + [Parameter(ValueFromPipeline = $true , Position = 0, DontShow = $true )] [hashtable] $ResponseList, @@ -2684,7 +2865,7 @@ function New-PodeOAResponse { else { $code = "$($StatusCode)" } - $response = @{} + $response = [ordered]@{} } process { foreach ($tag in $DefinitionTag) { @@ -2717,9 +2898,11 @@ function New-PodeOAResponse { .DESCRIPTION The New-PodeOAContentMediaType function generates media content type definitions suitable for use in OpenAPI specifications. It supports various media types and allows for the specification of content as either a single object or an array of objects. -.PARAMETER MediaType +.PARAMETER ContentType An array of strings specifying the media types to be defined. Media types should conform to standard MIME types (e.g., 'application/json', 'image/png'). The function validates these media types against a regular expression to ensure they are properly formatted. + Alias: MediaType + .PARAMETER Content The content definition for the media type. This could be an object representing the structure of the content expected for the specified media types. @@ -2754,20 +2937,20 @@ function New-PodeOAResponse { Set-PodeOARequest -PassThru -Parameters @( (New-PodeOAStringProperty -Name 'status' -Description 'Status values that need to be considered for filter' -Default 'available' -Enum @('available', 'pending', 'sold') | ConvertTo-PodeOAParameter -In Query) ) | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentMediaType 'application/json','application/xml' -Content 'Pet' -Array -UniqueItems) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json','application/xml' -Content 'Pet' -Array -UniqueItems) -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid status value' This example demonstrates the use of New-PodeOAContentMediaType in defining a GET route '/pet/findByStatus' in an OpenAPI specification. The route includes request parameters and responses with media content types for 'application/json' and 'application/xml'. .EXAMPLE - $content = @{ type = 'string' } + $content = [ordered]@{ type = 'string' } $mediaType = 'application/json' - New-PodeOAContentMediaType -MediaType $mediaType -Content $content + New-PodeOAContentMediaType -ContentType $mediaType -Content $content This example creates a media content type definition for 'application/json' with a simple string content type. .EXAMPLE - $content = @{ type = 'object'; properties = @{ name = @{ type = 'string' } } } + $content = [ordered]@{ type = 'object'; properties = [ordered]@{ name = @{ type = 'string' } } } $mediaTypes = 'application/json', 'application/xml' - New-PodeOAContentMediaType -MediaType $mediaTypes -Content $content -Array -MinItems 1 -MaxItems 5 -Title 'UserList' + New-PodeOAContentMediaType -ContentType $mediaTypes -Content $content -Array -MinItems 1 -MaxItems 5 -Title 'UserList' This example demonstrates defining an array of objects for both 'application/json' and 'application/xml' media types, with a specified range for the number of items and a title. .EXAMPLE @@ -2777,7 +2960,7 @@ function New-PodeOAResponse { Set-PodeOARequest -PassThru -Parameters @( (New-PodeOAStringProperty -Name 'status' -Description 'Status values that need to be considered for filter' -Default 'available' -Enum @('available', 'pending', 'sold') | ConvertTo-PodeOAParameter -In Query) ) | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentMediaType 'application/json','application/xml' -Content 'Pet' -Array -UniqueItems) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentType 'application/json','application/xml' -Content 'Pet' -Array -UniqueItems) -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid status value' This example demonstrates the use of New-PodeOAContentMediaType in defining a GET route '/pet/findByStatus' in an OpenAPI specification. The route includes request parameters and responses with media content types for 'application/json' and 'application/xml'. @@ -2789,8 +2972,10 @@ function New-PodeOAContentMediaType { [CmdletBinding(DefaultParameterSetName = 'inbuilt')] [OutputType([System.Collections.Specialized.OrderedDictionary])] param ( + [Parameter()] + [Alias('MediaType')] [string[]] - $MediaType = '*/*', + $ContentType = '*/*', [object] $Content, @@ -2832,20 +3017,21 @@ function New-PodeOAContentMediaType { $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag $props = [ordered]@{} - foreach ($media in $MediaType) { + foreach ($media in $ContentType) { if ($media -inotmatch '^(application|audio|image|message|model|multipart|text|video|\*)\/[\w\.\-\*]+(;[\s]*(charset|boundary)=[\w\.\-\*]+)*$') { - throw "Invalid content-type found for schema: $($media)" + # Invalid 'content-type' found for schema: $media + throw ($PodeLocale.invalidContentTypeForSchemaExceptionMessage -f $media) } if ($Upload.IsPresent) { if ( $media -ieq 'multipart/form-data' -and $Content) { - $Content = @{'__upload' = @{ + $Content = [ordered]@{'__upload' = [ordered]@{ 'content' = $Content 'partContentMediaType' = $PartContentMediaType } } } else { - $Content = @{'__upload' = @{ + $Content = [ordered]@{'__upload' = [ordered]@{ 'contentEncoding' = $ContentEncoding } } @@ -2854,7 +3040,7 @@ function New-PodeOAContentMediaType { } else { if ($null -eq $Content ) { - $Content = @{} + $Content = [ordered]@{} } } if ($Array.IsPresent) { @@ -2944,7 +3130,7 @@ function New-PodeOAResponseLink { [CmdletBinding(DefaultParameterSetName = 'OperationId')] [OutputType([System.Collections.Specialized.OrderedDictionary])] param( - [Parameter(ValueFromPipeline = $true , DontShow = $true )] + [Parameter(ValueFromPipeline = $true , Position = 0, DontShow = $true )] [System.Collections.Specialized.OrderedDictionary ] $LinkList, @@ -2992,12 +3178,12 @@ function New-PodeOAResponseLink { $DefinitionTag = $PodeContext.Server.OpenAPI.SelectedDefinitionTag } if ($Reference) { - Test-PodeOAComponentInternal -Field links -DefinitionTag $DefinitionTag -Name $Reference -PostValidation + Test-PodeOAComponentInternal -Field links -DefinitionTag $DefinitionTag -Name $Reference -PostValidation if (!$Name) { $Name = $Reference } $link = [ordered]@{ - $Name = @{ + $Name = [ordered]@{ '$ref' = "#/components/links/$Reference" } } @@ -3081,7 +3267,7 @@ function Add-PodeOAExternalRoute { [OutputType([hashtable[]], ParameterSetName = 'Pipeline')] [OutputType([hashtable], ParameterSetName = 'builtin')] param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Pipeline')] + [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0, ParameterSetName = 'Pipeline')] [ValidateNotNullOrEmpty()] [hashtable[]] $Route, @@ -3107,55 +3293,72 @@ function Add-PodeOAExternalRoute { [string[]] $DefinitionTag ) + begin { + # Initialize an array to hold piped-in values + $pipelineValue = @() + } - $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag + process { + if ($PSCmdlet.ParameterSetName -eq 'Pipeline') { + # Add the current piped-in value to the array + $pipelineValue += $_ + } + } - switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { - 'builtin' { - - # ensure the route has appropriate slashes - $Path = Update-PodeRouteSlashes -Path $Path - $OpenApiPath = ConvertTo-PodeOpenApiRoutePath -Path $Path - $Path = Resolve-PodePlaceholders -Path $Path - $extRoute = @{ - Method = $Method.ToLower() - Path = $Path - Local = $false - OpenApi = @{ - Path = $OpenApiPath - Responses = $null - Parameters = $null - RequestBody = $null - callbacks = [ordered]@{} - Authentication = @() - Servers = $Servers - DefinitionTag = $DefinitionTag - } - } - foreach ($tag in $DefinitionTag) { - #add the default OpenApi responses - if ( $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.defaultResponses) { - $extRoute.OpenApi.Responses = $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.defaultResponses.Clone() + end { + $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag + + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + 'builtin' { + + # ensure the route has appropriate slashes + $Path = Update-PodeRouteSlash -Path $Path + $OpenApiPath = ConvertTo-PodeOpenApiRoutePath -Path $Path + $Path = Resolve-PodePlaceholder -Path $Path + $extRoute = @{ + Method = $Method.ToLower() + Path = $Path + Local = $false + OpenApi = @{ + Path = $OpenApiPath + Responses = $null + Parameters = $null + RequestBody = $null + callbacks = [ordered]@{} + Authentication = @() + Servers = $Servers + DefinitionTag = $DefinitionTag + } } - if (! (Test-PodeOAComponentExternalPath -DefinitionTag $tag -Name $Path)) { - $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.externalPath[$Path] = @{} + foreach ($tag in $DefinitionTag) { + #add the default OpenApi responses + if ( $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.defaultResponses) { + $extRoute.OpenApi.Responses = Copy-PodeObjectDeepClone -InputObject $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.defaultResponses + } + if (! (Test-PodeOAComponentExternalPath -DefinitionTag $tag -Name $Path)) { + $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.externalPath[$Path] = [ordered]@{} + } + + $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.externalPath.$Path[$Method] = $extRoute } - $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.externalPath.$Path[$Method] = $extRoute + if ($PassThru) { + return $extRoute + } } - if ($PassThru) { - return $extRoute - } - } + 'pipeline' { + # Set Route to the array of values + if ($pipelineValue.Count -gt 1) { + $Route = $pipelineValue + } - 'pipeline' { - if ($null -eq $Route) { throw 'Add-PodeOAExternalRoute - The parameter -Route cannot be NULL.' } - foreach ($r in @($Route)) { - $r.OpenApi.Servers = $Servers - } - if ($PassThru) { - return $Route + foreach ($r in $Route) { + $r.OpenApi.Servers = $Servers + } + if ($PassThru) { + return $Route + } } } } @@ -3186,13 +3389,10 @@ New-PodeOAServerEndpoint -Url 'https://myserver.io/api' -Description 'My test se .EXAMPLE New-PodeOAServerEndpoint -Url '/api' -Description 'My local server' - - -} #> function New-PodeOAServerEndpoint { param ( - [Parameter(ValueFromPipeline = $true , DontShow = $true )] + [Parameter(ValueFromPipeline = $true , Position = 0, DontShow = $true )] [hashtable[]] $ServerEndpointList, @@ -3226,8 +3426,6 @@ function New-PodeOAServerEndpoint { } } - - <# .SYNOPSIS Sets metadate for the supplied route. @@ -3278,22 +3476,25 @@ function Add-PodeOAWebhook { $DefinitionTag ) - $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag + $_definitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag $refRoute = @{ Method = $Method.ToLower() NotPrepared = $true OpenApi = @{ - Responses = @{} - Parameters = $null - RequestBody = $null - callbacks = [ordered]@{} - Authentication = @() + Responses = [ordered]@{} + Parameters = $null + RequestBody = $null + callbacks = [ordered]@{} + Authentication = @() + DefinitionTag = $_definitionTag + IsDefTagConfigured = ($null -ne $DefinitionTag) #Definition Tag has been configured (Not default) } } - foreach ($tag in $DefinitionTag) { + foreach ($tag in $_definitionTag) { if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $tag ) { - throw 'The feature reusable component webhook is not available in OpenAPI v3.0.x' + # The Webhooks feature is not supported in OpenAPI v3.0.x + throw ($PodeLocale.webhooksFeatureNotSupportedInOpenApi30ExceptionMessage) } $PodeContext.Server.OpenAPI.Definitions[$tag].webhooks[$Name] = $refRoute } @@ -3303,7 +3504,6 @@ function Add-PodeOAWebhook { } } - <# .SYNOPSIS Select a group of OpenAPI Definions for modification. @@ -3331,7 +3531,7 @@ Select-PodeOADefinition -Tag 'v3', 'v3.1' -Script { New-PodeOAObjectProperty -XmlName 'order' | Add-PodeOAComponentSchema -Name 'Order' -New-PodeOAContentMediaType -ContentMediaType 'application/json', 'application/xml' -Content 'Pet' | +New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content 'Pet' | Add-PodeOAComponentRequestBody -Name 'Pet' -Description 'Pet object that needs to be added to the store' } @@ -3345,15 +3545,14 @@ function Select-PodeOADefinition { [Parameter(Mandatory = $true)] [scriptblock] $Scriptblock - - ) if (Test-PodeIsEmpty $Scriptblock) { - throw 'No scriptblock for -Scriptblock passed' + # No ScriptBlock supplied + throw ($PodeLocale.noScriptBlockSuppliedExceptionMessage) } if (Test-PodeIsEmpty -Value $Tag) { - $Tag = $PodeContext.Server.OpenAPI.DefaultDefinitionTag + $Tag = $PodeContext.Server.Web.OpenApi.DefaultDefinitionTag } else { $Tag = Test-PodeOADefinitionTag -Tag $Tag @@ -3364,15 +3563,79 @@ function Select-PodeOADefinition { $PodeContext.Server.OpenAPI.SelectedDefinitionTag = $Tag - $null = Invoke-PodeScriptBlock -ScriptBlock $Scriptblock -UsingVariables $usingVars -Splat + $null = Invoke-PodeScriptBlock -ScriptBlock $Scriptblock -UsingVariables $usingVars -Splat $PodeContext.Server.OpenAPI.SelectedDefinitionTag = $PodeContext.Server.OpenApi.DefinitionTagSelectionStack.Pop() } +<# +.SYNOPSIS +Renames an existing OpenAPI definition tag in Pode. + +.DESCRIPTION +This function renames an existing OpenAPI definition tag to a new tag name. +If the specified tag is the default definition tag, it updates the default tag as well. +It ensures that the new tag name does not already exist and that the function is not used within a Select-PodeOADefinition ScriptBlock. + +.PARAMETER Tag +The current tag name of the OpenAPI definition. If not specified, the default definition tag is used. + +.PARAMETER NewTag +The new tag name for the OpenAPI definition. This parameter is mandatory. + +.EXAMPLE +Rename-PodeOADefinitionTag -Tag 'oldTag' -NewTag 'newTag' + +Rename a specific OpenAPI definition tag +.EXAMPLE +Rename-PodeOADefinitionTag -NewTag 'newDefaultTag' +Rename the default OpenAPI definition tag +.NOTES +This function will throw an error if: +- It is used inside a Select-PodeOADefinition ScriptBlock. +- The new tag name already exists. +- The current tag name does not exist. +#> +function Rename-PodeOADefinitionTag { + param ( + [Parameter(Mandatory = $false)] + [string]$Tag, + [Parameter(Mandatory = $true)] + [string]$NewTag + ) + # Check if the function is being used inside a Select-PodeOADefinition ScriptBlock + if ($PodeContext.Server.OpenApi.DefinitionTagSelectionStack.Count -gt 0) { + throw ($PodeLocale.renamePodeOADefinitionTagExceptionMessage) + } + + # Check if the new tag name already exists in the OpenAPI definitions + if ($PodeContext.Server.OpenAPI.Definitions.ContainsKey($NewTag)) { + throw ($PodeLocale.openApiDefinitionAlreadyExistsExceptionMessage -f $NewTag ) + } + + # If the Tag parameter is null or whitespace, use the default definition tag + if ([string]::IsNullOrWhiteSpace($Tag)) { + $Tag = $PodeContext.Server.Web.OpenApi.DefaultDefinitionTag + $PodeContext.Server.Web.OpenApi.DefaultDefinitionTag = $NewTag # Update the default definition tag + } + else { + # Test if the specified tag exists in the OpenAPI definitions + Test-PodeOADefinitionTag -Tag $Tag + } + + # Rename the definition tag in the OpenAPI definitions + $PodeContext.Server.OpenAPI.Definitions[$NewTag] = $PodeContext.Server.OpenAPI.Definitions[$Tag] + $PodeContext.Server.OpenAPI.Definitions.Remove($Tag) + + # Update the selected definition tag if it matches the old tag + if ($PodeContext.Server.OpenAPI.SelectedDefinitionTag -eq $Tag) { + $PodeContext.Server.OpenAPI.SelectedDefinitionTag = $NewTag + } +} @@ -3401,7 +3664,8 @@ function Test-PodeOADefinitionTag { if ($Tag -and $Tag.Count -gt 0) { foreach ($t in $Tag) { if (! ($PodeContext.Server.OpenApi.Definitions.Keys -ccontains $t)) { - throw "DefinitionTag $t is not defined" + # DefinitionTag does not exist. + throw ($PodeLocale.definitionTagNotDefinedExceptionMessage -f $t) } } return $Tag @@ -3455,7 +3719,7 @@ function Test-PodeOADefinition { $result.issues[$tag] = @{ title = [string]::IsNullOrWhiteSpace( $PodeContext.Server.OpenAPI.Definitions[$tag].info.title) version = [string]::IsNullOrWhiteSpace( $PodeContext.Server.OpenAPI.Definitions[$tag].info.version) - components = @{} + components = [ordered]@{} definition = '' } foreach ($field in $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.postValidation.keys) { diff --git a/src/Public/Responses.ps1 b/src/Public/Responses.ps1 index e64eb7e55..9aedf0fd6 100644 --- a/src/Public/Responses.ps1 +++ b/src/Public/Responses.ps1 @@ -40,7 +40,7 @@ Set-PodeResponseAttachment -Path '/assets/data.txt' -EndpointName 'Example' function Set-PodeResponseAttachment { [CmdletBinding()] param ( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [string] $Path, @@ -56,23 +56,36 @@ function Set-PodeResponseAttachment { $FileBrowser ) + begin { + $pipelineItemCount = 0 + } - # already sent? skip - if ($WebEvent.Response.Sent) { - return + process { + $pipelineItemCount++ } - # only attach files from public/static-route directories when path is relative - $route = (Find-PodeStaticRoute -Path $Path -CheckPublic -EndpointName $EndpointName) - if ($route) { - $_path = $route.Content.Source + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + # already sent? skip + if ($WebEvent.Response.Sent) { + return + } + + # only attach files from public/static-route directories when path is relative + $route = (Find-PodeStaticRoute -Path $Path -CheckPublic -EndpointName $EndpointName) + if ($route) { + $_path = $route.Content.Source + + } + else { + $_path = Get-PodeRelativePath -Path $Path -JoinRoot + } + #call internal Attachment function + Write-PodeAttachmentResponseInternal -Path $_path -ContentType $ContentType -FileBrowser:$fileBrowser } - else { - $_path = Get-PodeRelativePath -Path $Path -JoinRoot - } - #call internal Attachment function - Write-PodeAttachmentResponseInternal -Path $_path -ContentType $ContentType -FileBrowser:$fileBrowser } @@ -139,156 +152,168 @@ function Write-PodeTextResponse { [switch] $Cache ) + begin { + # Initialize an array to hold piped-in values + $pipelineValue = @() + }process { + # Add the current piped-in value to the array + $pipelineValue += $_ + }end { + # Set Value to the array of values + if ($pipelineValue.Count -gt 1) { + $Value = $pipelineValue -join "`n" + } - $isStringValue = ($PSCmdlet.ParameterSetName -ieq 'string') - $isByteValue = ($PSCmdlet.ParameterSetName -ieq 'bytes') - - # set the status code of the response, but only if it's not 200 (to prevent overriding) - if ($StatusCode -ne 200) { - Set-PodeResponseStatus -Code $StatusCode -NoErrorPage - } + $isStringValue = ($PSCmdlet.ParameterSetName -ieq 'string') + $isByteValue = ($PSCmdlet.ParameterSetName -ieq 'bytes') - # if there's nothing to write, return - if ($isStringValue -and [string]::IsNullOrWhiteSpace($Value)) { - return - } + # set the status code of the response, but only if it's not 200 (to prevent overriding) + if ($StatusCode -ne 200) { + Set-PodeResponseStatus -Code $StatusCode -NoErrorPage + } - if ($isByteValue -and (($null -eq $Bytes) -or ($Bytes.Length -eq 0))) { - return - } + # if there's nothing to write, return + if ($isStringValue -and [string]::IsNullOrWhiteSpace($Value)) { + return + } - # if the response stream isn't writable or already sent, return - $res = $WebEvent.Response - if (($null -eq $res) -or ($WebEvent.Streamed -and (($null -eq $res.OutputStream) -or !$res.OutputStream.CanWrite -or $res.Sent))) { - return - } + if ($isByteValue -and (($null -eq $Bytes) -or ($Bytes.Length -eq 0))) { + return + } - # set a cache value - if ($Cache) { - Set-PodeHeader -Name 'Cache-Control' -Value "max-age=$($MaxAge), must-revalidate" - Set-PodeHeader -Name 'Expires' -Value ([datetime]::UtcNow.AddSeconds($MaxAge).ToString('r', [CultureInfo]::InvariantCulture)) - } + # if the response stream isn't writable or already sent, return + $res = $WebEvent.Response + if (($null -eq $res) -or ($WebEvent.Streamed -and (($null -eq $res.OutputStream) -or !$res.OutputStream.CanWrite -or $res.Sent))) { + return + } - # specify the content-type if supplied (adding utf-8 if missing) - if (![string]::IsNullOrWhiteSpace($ContentType)) { - $charset = 'charset=utf-8' - if ($ContentType -inotcontains $charset) { - $ContentType = "$($ContentType); $($charset)" + # set a cache value + if ($Cache) { + Set-PodeHeader -Name 'Cache-Control' -Value "max-age=$($MaxAge), must-revalidate" + Set-PodeHeader -Name 'Expires' -Value ([datetime]::UtcNow.AddSeconds($MaxAge).ToString('r', [CultureInfo]::InvariantCulture)) } - $res.ContentType = $ContentType - } + # specify the content-type if supplied (adding utf-8 if missing) + if (![string]::IsNullOrWhiteSpace($ContentType)) { + $charset = 'charset=utf-8' + if ($ContentType -inotcontains $charset) { + $ContentType = "$($ContentType); $($charset)" + } - # if we're serverless, set the string as the body - if (!$WebEvent.Streamed) { - if ($isStringValue) { - $res.Body = $Value - } - else { - $res.Body = $Bytes + $res.ContentType = $ContentType } - } - else { - # convert string to bytes - if ($isStringValue) { - $Bytes = ConvertFrom-PodeValueToBytes -Value $Value + # if we're serverless, set the string as the body + if (!$WebEvent.Streamed) { + if ($isStringValue) { + $res.Body = $Value + } + else { + $res.Body = $Bytes + } } - # check if we only need a range of the bytes - if (($null -ne $WebEvent.Ranges) -and ($WebEvent.Response.StatusCode -eq 200) -and ($StatusCode -eq 200)) { - $lengths = @() - $size = $Bytes.Length + else { + # convert string to bytes + if ($isStringValue) { + $Bytes = ConvertFrom-PodeValueToByteArray -Value $Value + } - $Bytes = @(foreach ($range in $WebEvent.Ranges) { - # ensure range not invalid - if (([int]$range.Start -lt 0) -or ([int]$range.Start -ge $size) -or ([int]$range.End -lt 0)) { - Set-PodeResponseStatus -Code 416 -NoErrorPage - return - } + # check if we only need a range of the bytes + if (($null -ne $WebEvent.Ranges) -and ($WebEvent.Response.StatusCode -eq 200) -and ($StatusCode -eq 200)) { + $lengths = @() + $size = $Bytes.Length - # skip start bytes only - if ([string]::IsNullOrWhiteSpace($range.End)) { - $Bytes[$range.Start..($size - 1)] - $lengths += "$($range.Start)-$($size - 1)/$($size)" - } + $Bytes = @(foreach ($range in $WebEvent.Ranges) { + # ensure range not invalid + if (([int]$range.Start -lt 0) -or ([int]$range.Start -ge $size) -or ([int]$range.End -lt 0)) { + Set-PodeResponseStatus -Code 416 -NoErrorPage + return + } - # end bytes only - elseif ([string]::IsNullOrWhiteSpace($range.Start)) { - if ([int]$range.End -gt $size) { - $range.End = $size + # skip start bytes only + if ([string]::IsNullOrWhiteSpace($range.End)) { + $Bytes[$range.Start..($size - 1)] + $lengths += "$($range.Start)-$($size - 1)/$($size)" } - if ([int]$range.End -gt 0) { - $Bytes[$($size - $range.End)..($size - 1)] - $lengths += "$($size - $range.End)-$($size - 1)/$($size)" + # end bytes only + elseif ([string]::IsNullOrWhiteSpace($range.Start)) { + if ([int]$range.End -gt $size) { + $range.End = $size + } + + if ([int]$range.End -gt 0) { + $Bytes[$($size - $range.End)..($size - 1)] + $lengths += "$($size - $range.End)-$($size - 1)/$($size)" + } + else { + $lengths += "0-0/$($size)" + } } + + # normal range else { - $lengths += "0-0/$($size)" - } - } + if ([int]$range.End -ge $size) { + Set-PodeResponseStatus -Code 416 -NoErrorPage + return + } - # normal range - else { - if ([int]$range.End -ge $size) { - Set-PodeResponseStatus -Code 416 -NoErrorPage - return + $Bytes[$range.Start..$range.End] + $lengths += "$($range.Start)-$($range.End)/$($size)" } + }) - $Bytes[$range.Start..$range.End] - $lengths += "$($range.Start)-$($range.End)/$($size)" - } - }) - - Set-PodeHeader -Name 'Content-Range' -Value "bytes $($lengths -join ', ')" - if ($StatusCode -eq 200) { - Set-PodeResponseStatus -Code 206 -NoErrorPage + Set-PodeHeader -Name 'Content-Range' -Value "bytes $($lengths -join ', ')" + if ($StatusCode -eq 200) { + Set-PodeResponseStatus -Code 206 -NoErrorPage + } } - } - # check if we need to compress the response - if ($PodeContext.Server.Web.Compression.Enabled -and ![string]::IsNullOrWhiteSpace($WebEvent.AcceptEncoding)) { - try { - $ms = New-Object -TypeName System.IO.MemoryStream - $stream = New-Object "System.IO.Compression.$($WebEvent.AcceptEncoding)Stream"($ms, [System.IO.Compression.CompressionMode]::Compress, $true) - $stream.Write($Bytes, 0, $Bytes.Length) - $stream.Close() - $ms.Position = 0 - $Bytes = $ms.ToArray() - } - finally { - if ($null -ne $stream) { + # check if we need to compress the response + if ($PodeContext.Server.Web.Compression.Enabled -and ![string]::IsNullOrWhiteSpace($WebEvent.AcceptEncoding)) { + try { + $ms = [System.IO.MemoryStream]::new() + $stream = Get-PodeCompressionStream -InputStream $ms -Encoding $WebEvent.AcceptEncoding -Mode Compress + $stream.Write($Bytes, 0, $Bytes.Length) $stream.Close() + $ms.Position = 0 + $Bytes = $ms.ToArray() } + finally { + if ($null -ne $stream) { + $stream.Close() + } - if ($null -ne $ms) { - $ms.Close() + if ($null -ne $ms) { + $ms.Close() + } } - } - # set content encoding header - Set-PodeHeader -Name 'Content-Encoding' -Value $WebEvent.AcceptEncoding - } + # set content encoding header + Set-PodeHeader -Name 'Content-Encoding' -Value $WebEvent.AcceptEncoding + } - # write the content to the response stream - $res.ContentLength64 = $Bytes.Length + # write the content to the response stream + $res.ContentLength64 = $Bytes.Length - try { - $ms = New-Object -TypeName System.IO.MemoryStream - $ms.Write($Bytes, 0, $Bytes.Length) - $ms.WriteTo($res.OutputStream) - } - catch { - if ((Test-PodeValidNetworkFailure $_.Exception)) { - return + try { + $ms = [System.IO.MemoryStream]::new() + $ms.Write($Bytes, 0, $Bytes.Length) + $ms.WriteTo($res.OutputStream) } + catch { + if ((Test-PodeValidNetworkFailure $_.Exception)) { + return + } - $_ | Write-PodeErrorLog - throw - } - finally { - if ($null -ne $ms) { - $ms.Close() + $_ | Write-PodeErrorLog + throw + } + finally { + if ($null -ne $ms) { + $ms.Close() + } } } } @@ -344,7 +369,7 @@ Write-PodeFileResponse -Path 'C:/Files/' -FileBrowser function Write-PodeFileResponse { [CmdletBinding()] param ( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [ValidateNotNull()] [string] $Path, @@ -370,12 +395,24 @@ function Write-PodeFileResponse { [switch] $FileBrowser ) + begin { + $pipelineItemCount = 0 + } + + process { + $pipelineItemCount++ + } - # resolve for relative path - $RelativePath = Get-PodeRelativePath -Path $Path -JoinRoot + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + # resolve for relative path + $RelativePath = Get-PodeRelativePath -Path $Path -JoinRoot - Write-PodeFileResponseInternal -Path $RelativePath -Data $Data -ContentType $ContentType -MaxAge $MaxAge ` - -StatusCode $StatusCode -Cache:$Cache -FileBrowser:$FileBrowser + Write-PodeFileResponseInternal -Path $RelativePath -Data $Data -ContentType $ContentType -MaxAge $MaxAge ` + -StatusCode $StatusCode -Cache:$Cache -FileBrowser:$FileBrowser + } } <# @@ -399,22 +436,35 @@ Generates and serves an HTML page that lists the contents of the './static' dire function Write-PodeDirectoryResponse { [CmdletBinding()] param ( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [ValidateNotNull()] [string] $Path ) + begin { + $pipelineItemCount = 0 + } - # resolve for relative path - $RelativePath = Get-PodeRelativePath -Path $Path -JoinRoot - - if (Test-Path -Path $RelativePath -PathType Container) { - Write-PodeDirectoryResponseInternal -Path $RelativePath + process { + $pipelineItemCount++ } - else { - Set-PodeResponseStatus -Code 404 + + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + # resolve for relative path + $RelativePath = Get-PodeRelativePath -Path $Path -JoinRoot + + if (Test-Path -Path $RelativePath -PathType Container) { + Write-PodeDirectoryResponseInternal -Path $RelativePath + } + else { + Set-PodeResponseStatus -Code 404 + } } } + <# .SYNOPSIS Writes CSV data to the Response. @@ -455,39 +505,52 @@ function Write-PodeCsvResponse { $StatusCode = 200 ) - switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { - 'file' { - if (Test-PodePath $Path) { - $Value = Get-PodeFileContent -Path $Path - } - } + begin { + $pipelineValue = @() + } - 'value' { - if ($Value -isnot [string]) { - $Value = @(foreach ($v in $Value) { - New-Object psobject -Property $v - }) + process { + if ($PSCmdlet.ParameterSetName -eq 'Value') { + $pipelineValue += $_ + } + } - if (Test-PodeIsPSCore) { - $Value = ($Value | ConvertTo-Csv -Delimiter ',' -IncludeTypeInformation:$false) + end { + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + 'file' { + if (Test-PodePath $Path) { + $Value = Get-PodeFileContent -Path $Path } - else { - $Value = ($Value | ConvertTo-Csv -Delimiter ',' -NoTypeInformation) + } + + 'value' { + if ($pipelineValue.Count -gt 1) { + $Value = $pipelineValue } - $Value = ($Value -join ([environment]::NewLine)) + if ($Value -isnot [string]) { + $Value = Resolve-PodeObjectArray -Property $Value + + if (Test-PodeIsPSCore) { + $Value = ($Value | ConvertTo-Csv -Delimiter ',' -IncludeTypeInformation:$false) + } + else { + $Value = ($Value | ConvertTo-Csv -Delimiter ',' -NoTypeInformation) + } + + $Value = ($Value -join ([environment]::NewLine)) + } } } - } - if ([string]::IsNullOrWhiteSpace($Value)) { - $Value = [string]::Empty - } + if ([string]::IsNullOrWhiteSpace($Value)) { + $Value = [string]::Empty + } - Write-PodeTextResponse -Value $Value -ContentType 'text/csv' -StatusCode $StatusCode + Write-PodeTextResponse -Value $Value -ContentType 'text/csv' -StatusCode $StatusCode + } } - <# .SYNOPSIS Writes HTML data to the Response. @@ -528,26 +591,41 @@ function Write-PodeHtmlResponse { $StatusCode = 200 ) - switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { - 'file' { - if (Test-PodePath $Path) { - $Value = Get-PodeFileContent -Path $Path - } + begin { + $pipelineValue = @() + } + + process { + if ($PSCmdlet.ParameterSetName -eq 'Value') { + $pipelineValue += $_ } + } - 'value' { - if ($Value -isnot [string]) { - $Value = ($Value | ConvertTo-Html) - $Value = ($Value -join ([environment]::NewLine)) + end { + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + 'file' { + if (Test-PodePath $Path) { + $Value = Get-PodeFileContent -Path $Path + } + } + + 'value' { + if ($pipelineValue.Count -gt 1) { + $Value = $pipelineValue + } + if ($Value -isnot [string]) { + $Value = ($Value | ConvertTo-Html) + $Value = ($Value -join ([environment]::NewLine)) + } } } - } - if ([string]::IsNullOrWhiteSpace($Value)) { - $Value = [string]::Empty - } + if ([string]::IsNullOrWhiteSpace($Value)) { + $Value = [string]::Empty + } - Write-PodeTextResponse -Value $Value -ContentType 'text/html' -StatusCode $StatusCode + Write-PodeTextResponse -Value $Value -ContentType 'text/html' -StatusCode $StatusCode + } } @@ -593,29 +671,41 @@ function Write-PodeMarkdownResponse { [switch] $AsHtml ) + begin { + $pipelineItemCount = 0 + } - switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { - 'file' { - if (Test-PodePath $Path) { - $Value = Get-PodeFileContent -Path $Path + process { + $pipelineItemCount++ + } + + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + 'file' { + if (Test-PodePath $Path) { + $Value = Get-PodeFileContent -Path $Path + } } } - } - if ([string]::IsNullOrWhiteSpace($Value)) { - $Value = [string]::Empty - } + if ([string]::IsNullOrWhiteSpace($Value)) { + $Value = [string]::Empty + } - $mimeType = 'text/markdown' + $mimeType = 'text/markdown' - if ($AsHtml) { - if ($PSVersionTable.PSVersion.Major -ge 7) { - $mimeType = 'text/html' - $Value = ($Value | ConvertFrom-Markdown).Html + if ($AsHtml) { + if ($PSVersionTable.PSVersion.Major -ge 7) { + $mimeType = 'text/html' + $Value = ($Value | ConvertFrom-Markdown).Html + } } - } - Write-PodeTextResponse -Value $Value -ContentType $mimeType -StatusCode $StatusCode + Write-PodeTextResponse -Value $Value -ContentType $mimeType -StatusCode $StatusCode + } } <# @@ -631,6 +721,10 @@ A String, PSObject, or HashTable value. For non-string values, they will be conv .PARAMETER Path The path to a JSON file. +.PARAMETER ContentType +Because JSON content has not yet an official content type. one custom can be specified here (Default: 'application/json' ) +https://www.rfc-editor.org/rfc/rfc8259 + .PARAMETER Depth The Depth to generate the JSON document - the larger this value the worse performance gets. @@ -660,6 +754,12 @@ function Write-PodeJsonResponse { [string] $Path, + [Parameter()] + [ValidatePattern('^\w+\/[\w\.\+-]+$')] + [ValidateNotNullOrEmpty()] + [string] + $ContentType = 'application/json', + [Parameter(ParameterSetName = 'Value')] [ValidateRange(0, 100)] [int] @@ -674,34 +774,43 @@ function Write-PodeJsonResponse { $NoCompress ) + begin { + $pipelineValue = @() + } - switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { - 'file' { - if (Test-PodePath $Path) { - $Value = Get-PodeFileContent -Path $Path - } - if ([string]::IsNullOrWhiteSpace($Value)) { - $Value = '{}' - } + process { + if ($PSCmdlet.ParameterSetName -eq 'Value') { + $pipelineValue += $_ } + } + + end { + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + 'file' { + if (Test-PodePath $Path) { + $Value = Get-PodeFileContent -Path $Path + } + if ([string]::IsNullOrWhiteSpace($Value)) { + $Value = '{}' + } + } - 'value' { - if ($Value -isnot [string]) { - if ($Depth -le 0) { - $Value = (ConvertTo-Json -InputObject $Value -Compress:(!$NoCompress)) + 'value' { + if ($pipelineValue.Count -gt 1) { + $Value = $pipelineValue } - else { + if ($Value -isnot [string]) { $Value = (ConvertTo-Json -InputObject $Value -Depth $Depth -Compress:(!$NoCompress)) } } } - } - if ([string]::IsNullOrWhiteSpace($Value)) { - $Value = '{}' - } + if ([string]::IsNullOrWhiteSpace($Value)) { + $Value = '{}' + } - Write-PodeTextResponse -Value $Value -ContentType 'application/json' -StatusCode $StatusCode + Write-PodeTextResponse -Value $Value -ContentType $ContentType -StatusCode $StatusCode + } } @@ -718,6 +827,13 @@ A String, PSObject, or HashTable value. .PARAMETER Path The path to an XML file. +.PARAMETER ContentType +Because XML content has not yet an official content type. one custom can be specified here (Default: 'application/xml' ) +https://www.rfc-editor.org/rfc/rfc3023 + +.PARAMETER Depth +The Depth to generate the XML document - the larger this value the worse performance gets. + .PARAMETER StatusCode The status code to set against the response. @@ -727,8 +843,29 @@ Write-PodeXmlResponse -Value 'Rick' .EXAMPLE Write-PodeXmlResponse -Value @{ Name = 'Rick' } -StatusCode 201 +.EXAMPLE +@(@{ Name = 'Rick' }, @{ Name = 'Don' }) | Write-PodeXmlResponse + +.EXAMPLE +$users = @([PSCustomObject]@{ + Name = 'Rick' + }, [PSCustomObject]@{ + Name = 'Don' + } + ) +Write-PodeXmlResponse -Value $users + +.EXAMPLE +@([PSCustomObject]@{ + Name = 'Rick' + }, [PSCustomObject]@{ + Name = 'Don' + } +) | Write-PodeXmlResponse + .EXAMPLE Write-PodeXmlResponse -Path 'E:/Files/Names.xml' + #> function Write-PodeXmlResponse { [CmdletBinding(DefaultParameterSetName = 'Value')] @@ -741,34 +878,57 @@ function Write-PodeXmlResponse { [string] $Path, + [Parameter(ParameterSetName = 'Value')] + [ValidateRange(0, 100)] + [int] + $Depth = 10, + + [Parameter()] + [ValidatePattern('^\w+\/[\w\.\+-]+$')] + [ValidateNotNullOrEmpty()] + [string] + $ContentType = 'application/xml', + [Parameter()] [int] $StatusCode = 200 ) + begin { + $pipelineValue = @() + } - switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { - 'file' { - if (Test-PodePath $Path) { - $Value = Get-PodeFileContent -Path $Path - } + process { + if ($PSCmdlet.ParameterSetName -eq 'Value' -and $_) { + $pipelineValue += $_ } + } - 'value' { - if ($Value -isnot [string]) { - $Value = @(foreach ($v in $Value) { - New-Object psobject -Property $v - }) + end { + + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + 'file' { + if (Test-PodePath $Path) { + $Value = Get-PodeFileContent -Path $Path + } + } - $Value = ($Value | ConvertTo-Xml -Depth 10 -As String -NoTypeInformation) + 'value' { + if ($pipelineValue.Count -gt 1) { + $Value = $pipelineValue + } + + if ($Value -isnot [string]) { + $Value = Resolve-PodeObjectArray -Property $Value | ConvertTo-Xml -Depth $Depth -As String -NoTypeInformation + } } } - } - if ([string]::IsNullOrWhiteSpace($Value)) { - $Value = [string]::Empty - } + if ([string]::IsNullOrWhiteSpace($Value)) { + $Value = [string]::Empty + } - Write-PodeTextResponse -Value $Value -ContentType 'text/xml' -StatusCode $StatusCode + Write-PodeTextResponse -Value $Value -ContentType $ContentType -StatusCode $StatusCode + } } <# @@ -785,7 +945,8 @@ A String, PSObject, or HashTable value. For non-string values, they will be conv The path to a YAML file. .PARAMETER ContentType -Because JSON content has not yet an official content type. one custom can be specified here (Default: 'application/x-yaml' ) +Because YAML content has not yet an official content type. one custom can be specified here (Default: 'application/yaml' ) +https://www.rfc-editor.org/rfc/rfc9512 .PARAMETER Depth The Depth to generate the YAML document - the larger this value the worse performance gets. @@ -817,7 +978,7 @@ function Write-PodeYamlResponse { [ValidatePattern('^\w+\/[\w\.\+-]+$')] [ValidateNotNullOrEmpty()] [string] - $ContentType = 'application/x-yaml', + $ContentType = 'application/yaml', [Parameter(ParameterSetName = 'Value')] @@ -830,30 +991,42 @@ function Write-PodeYamlResponse { $StatusCode = 200 ) - switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { - 'file' { - if (Test-PodePath $Path) { - $Value = Get-PodeFileContent -Path $Path - } + begin { + $pipelineValue = @() + } + + process { + if ($PSCmdlet.ParameterSetName -eq 'Value') { + $pipelineValue += $_ } + } - 'value' { - if ($Value -isnot [string]) { - if ( $Depth -gt 0) { - $Value = ConvertTo-PodeYaml -InputObject $Value -Depth $Depth + end { + + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + 'file' { + if (Test-PodePath $Path) { + $Value = Get-PodeFileContent -Path $Path + } + } + + 'value' { + if ($pipelineValue.Count -gt 1) { + $Value = $pipelineValue } - else { - $Value = ConvertTo-PodeYaml -InputObject $Value + + if ($Value -isnot [string]) { + $Value = ConvertTo-PodeYaml -InputObject $Value -Depth $Depth + } } } - } - if ([string]::IsNullOrWhiteSpace($Value)) { - $Value = '[]' - } - - Write-PodeTextResponse -Value $Value -ContentType $ContentType -StatusCode $StatusCode + if ([string]::IsNullOrWhiteSpace($Value)) { + $Value = '[]' + } + Write-PodeTextResponse -Value $Value -ContentType $ContentType -StatusCode $StatusCode + } } @@ -892,7 +1065,7 @@ Write-PodeViewResponse -Path 'login' -FlashMessages function Write-PodeViewResponse { [CmdletBinding()] param ( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [string] $Path, @@ -911,59 +1084,71 @@ function Write-PodeViewResponse { [switch] $FlashMessages ) - - # default data if null - if ($null -eq $Data) { - $Data = @{} + begin { + $pipelineItemCount = 0 } - # add path to data as "pagename" - unless key already exists - if (!$Data.ContainsKey('pagename')) { - $Data['pagename'] = $Path + process { + $pipelineItemCount++ } - # load all flash messages if needed - if ($FlashMessages -and ($null -ne $WebEvent.Session.Data.Flash)) { - $Data['flash'] = @{} + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + # default data if null + if ($null -eq $Data) { + $Data = @{} + } - foreach ($name in (Get-PodeFlashMessageNames)) { - $Data.flash[$name] = (Get-PodeFlashMessage -Name $name) + # add path to data as "pagename" - unless key already exists + if (!$Data.ContainsKey('pagename')) { + $Data['pagename'] = $Path } - } - elseif ($null -eq $Data['flash']) { - $Data['flash'] = @{} - } - # add view engine extension - $ext = Get-PodeFileExtension -Path $Path - if ([string]::IsNullOrWhiteSpace($ext)) { - $Path += ".$($PodeContext.Server.ViewEngine.Extension)" - } + # load all flash messages if needed + if ($FlashMessages -and ($null -ne $WebEvent.Session.Data.Flash)) { + $Data['flash'] = @{} - # only look in the view directories - $viewFolder = $PodeContext.Server.InbuiltDrives['views'] - if (![string]::IsNullOrWhiteSpace($Folder)) { - $viewFolder = $PodeContext.Server.Views[$Folder] - } + foreach ($name in (Get-PodeFlashMessageNames)) { + $Data.flash[$name] = (Get-PodeFlashMessage -Name $name) + } + } + elseif ($null -eq $Data['flash']) { + $Data['flash'] = @{} + } - $Path = [System.IO.Path]::Combine($viewFolder, $Path) + # add view engine extension + $ext = Get-PodeFileExtension -Path $Path + if ([string]::IsNullOrWhiteSpace($ext)) { + $Path += ".$($PodeContext.Server.ViewEngine.Extension)" + } - # test the file path, and set status accordingly - if (!(Test-PodePath $Path)) { - return - } + # only look in the view directories + $viewFolder = $PodeContext.Server.InbuiltDrives['views'] + if (![string]::IsNullOrWhiteSpace($Folder)) { + $viewFolder = $PodeContext.Server.Views[$Folder] + } - # run any engine logic and render it - $engine = (Get-PodeViewEngineType -Path $Path) - $value = (Get-PodeFileContentUsingViewEngine -Path $Path -Data $Data) + $Path = [System.IO.Path]::Combine($viewFolder, $Path) - switch ($engine.ToLowerInvariant()) { - 'md' { - Write-PodeMarkdownResponse -Value $value -StatusCode $StatusCode -AsHtml + # test the file path, and set status accordingly + if (!(Test-PodePath $Path)) { + return } - default { - Write-PodeHtmlResponse -Value $value -StatusCode $StatusCode + # run any engine logic and render it + $engine = (Get-PodeViewEngineType -Path $Path) + $value = (Get-PodeFileContentUsingViewEngine -Path $Path -Data $Data) + + switch ($engine.ToLowerInvariant()) { + 'md' { + Write-PodeMarkdownResponse -Value $value -StatusCode $StatusCode -AsHtml + } + + default { + Write-PodeHtmlResponse -Value $value -StatusCode $StatusCode + } } } } @@ -1182,8 +1367,23 @@ function Write-PodeTcpClient { [string] $Message ) + begin { + # Initialize an array to hold piped-in values + $pipelineValue = @() + } - $TcpEvent.Response.WriteLine($Message, $true) + process { + # Add the current piped-in value to the array + $pipelineValue += $_ + } + + end { + # Set Route to the array of values + if ($pipelineValue.Count -gt 1) { + $Message = $pipelineValue -join "`n" + } + $TcpEvent.Response.WriteLine($Message, $true) + } } <# @@ -1297,7 +1497,8 @@ function Save-PodeRequestFile { # ensure the parameter name exists in data if (!(Test-PodeRequestFile -Key $Key)) { - throw "A parameter called '$($Key)' was not supplied in the request, or has no data available" + # A parameter called was not supplied in the request or has no data available + throw ($PodeLocale.parameterNotSuppliedInRequestExceptionMessage -f $Key) } # get the file names @@ -1313,7 +1514,8 @@ function Save-PodeRequestFile { # ensure the file data exists foreach ($file in $files) { if (!$WebEvent.Files.ContainsKey($file)) { - throw "No data for file '$($file)' was uploaded in the request" + # No data for file was uploaded in the request + throw ($PodeLocale.noDataForFileUploadedExceptionMessage -f $file) } } @@ -1468,7 +1670,7 @@ function Use-PodePartialView { [CmdletBinding()] [OutputType([string])] param ( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] [string] $Path, @@ -1479,32 +1681,45 @@ function Use-PodePartialView { [string] $Folder ) - - # default data if null - if ($null -eq $Data) { - $Data = @{} - } - # add view engine extension - $ext = Get-PodeFileExtension -Path $Path - if ([string]::IsNullOrWhiteSpace($ext)) { - $Path += ".$($PodeContext.Server.ViewEngine.Extension)" + begin { + $pipelineItemCount = 0 } - # only look in the view directory - $viewFolder = $PodeContext.Server.InbuiltDrives['views'] - if (![string]::IsNullOrWhiteSpace($Folder)) { - $viewFolder = $PodeContext.Server.Views[$Folder] + process { + $pipelineItemCount++ } - $Path = [System.IO.Path]::Combine($viewFolder, $Path) + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + # default data if null + if ($null -eq $Data) { + $Data = @{} + } + # add view engine extension + $ext = Get-PodeFileExtension -Path $Path + if ([string]::IsNullOrWhiteSpace($ext)) { + $Path += ".$($PodeContext.Server.ViewEngine.Extension)" + } + + # only look in the view directory + $viewFolder = $PodeContext.Server.InbuiltDrives['views'] + if (![string]::IsNullOrWhiteSpace($Folder)) { + $viewFolder = $PodeContext.Server.Views[$Folder] + } + + $Path = [System.IO.Path]::Combine($viewFolder, $Path) - # test the file path, and set status accordingly - if (!(Test-PodePath $Path -NoStatus)) { - throw "File not found at path: $($Path)" - } + # test the file path, and set status accordingly + if (!(Test-PodePath $Path -NoStatus)) { + # The Views path does not exist + throw ($PodeLocale.viewsPathDoesNotExistExceptionMessage -f $Path) + } - # run any engine logic - return (Get-PodeFileContentUsingViewEngine -Path $Path -Data $Data) + # run any engine logic + return (Get-PodeFileContentUsingViewEngine -Path $Path -Data $Data) + } } <# @@ -1541,7 +1756,7 @@ Send-PodeSignal -Value @{ Data = @(123, 100, 101) } -Path '/response-charts' function Send-PodeSignal { [CmdletBinding()] param( - [Parameter(ValueFromPipeline = $true)] + [Parameter(ValueFromPipeline = $true, Position = 0 )] $Value, [Parameter()] @@ -1564,47 +1779,61 @@ function Send-PodeSignal { [switch] $IgnoreEvent ) - # error if not configured - if (!$PodeContext.Server.Signals.Enabled) { - throw 'WebSockets have not been configured to send signal messages' + begin { + $pipelineItemCount = 0 } - # do nothing if no value - if (($null -eq $Value) -or ([string]::IsNullOrEmpty($Value))) { - return + process { + $pipelineItemCount++ } - # jsonify the value - if ($Value -isnot [string]) { - if ($Depth -le 0) { - $Value = (ConvertTo-Json -InputObject $Value -Compress) + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) } - else { - $Value = (ConvertTo-Json -InputObject $Value -Depth $Depth -Compress) + # error if not configured + if (!$PodeContext.Server.Signals.Enabled) { + # WebSockets have not been configured to send signal messages + throw ($PodeLocale.websocketsNotConfiguredForSignalMessagesExceptionMessage) } - } - # check signal event - if (!$IgnoreEvent -and ($null -ne $SignalEvent)) { - if ([string]::IsNullOrWhiteSpace($Path)) { - $Path = $SignalEvent.Data.Path + # do nothing if no value + if (($null -eq $Value) -or ([string]::IsNullOrEmpty($Value))) { + return } - if ([string]::IsNullOrWhiteSpace($ClientId)) { - $ClientId = $SignalEvent.Data.ClientId + # jsonify the value + if ($Value -isnot [string]) { + if ($Depth -le 0) { + $Value = (ConvertTo-Json -InputObject $Value -Compress) + } + else { + $Value = (ConvertTo-Json -InputObject $Value -Depth $Depth -Compress) + } } - if (($Mode -ieq 'Auto') -and ($SignalEvent.Data.Direct -or ($SignalEvent.ClientId -ieq $SignalEvent.Data.ClientId))) { - $Mode = 'Direct' + # check signal event + if (!$IgnoreEvent -and ($null -ne $SignalEvent)) { + if ([string]::IsNullOrWhiteSpace($Path)) { + $Path = $SignalEvent.Data.Path + } + + if ([string]::IsNullOrWhiteSpace($ClientId)) { + $ClientId = $SignalEvent.Data.ClientId + } + + if (($Mode -ieq 'Auto') -and ($SignalEvent.Data.Direct -or ($SignalEvent.ClientId -ieq $SignalEvent.Data.ClientId))) { + $Mode = 'Direct' + } } - } - # broadcast or direct? - if ($Mode -iin @('Auto', 'Broadcast')) { - $PodeContext.Server.Signals.Listener.AddServerSignal($Value, $Path, $ClientId) - } - else { - $SignalEvent.Response.Write($Value) + # broadcast or direct? + if ($Mode -iin @('Auto', 'Broadcast')) { + $PodeContext.Server.Signals.Listener.AddServerSignal($Value, $Path, $ClientId) + } + else { + $SignalEvent.Response.Write($Value) + } } } @@ -1638,13 +1867,15 @@ function Add-PodeViewFolder { # ensure the folder doesn't already exist if ($PodeContext.Server.Views.ContainsKey($Name)) { - throw "The Views folder name already exists: $($Name)" + # The Views folder name already exists + throw ($PodeLocale.viewsFolderNameAlreadyExistsExceptionMessage -f $Name) } # ensure the path exists at server root $Source = Get-PodeRelativePath -Path $Source -JoinRoot if (!(Test-PodePath -Path $Source -NoStatus)) { - throw "The Views path does not exist: $($Source)" + # The Views path does not exist + throw ($PodeLocale.viewsPathDoesNotExistExceptionMessage -f $Source) } # setup a temp drive for the path @@ -1670,6 +1901,6 @@ function Send-PodeResponse { param() if ($null -ne $WebEvent.Response) { - $WebEvent.Response.Send() + $null = Wait-PodeTask -Task $WebEvent.Response.Send() } } \ No newline at end of file diff --git a/src/Public/Routes.ps1 b/src/Public/Routes.ps1 index 716d56b97..9af545f77 100644 --- a/src/Public/Routes.ps1 +++ b/src/Public/Routes.ps1 @@ -284,16 +284,17 @@ function Add-PodeRoute { # split route on '?' for query $Path = Split-PodeRouteQuery -Path $Path if ([string]::IsNullOrWhiteSpace($Path)) { - throw 'No Path supplied for Route' + # No Path supplied for the Route + throw ($PodeLocale.noPathSuppliedForRouteExceptionMessage) } # ensure the route has appropriate slashes - $Path = Update-PodeRouteSlashes -Path $Path + $Path = Update-PodeRouteSlash -Path $Path $OpenApiPath = ConvertTo-PodeOpenApiRoutePath -Path $Path - $Path = Resolve-PodePlaceholders -Path $Path + $Path = Resolve-PodePlaceholder -Path $Path # get endpoints from name - $endpoints = Find-PodeEndpoints -EndpointName $EndpointName + $endpoints = Find-PodeEndpoint -EndpointName $EndpointName # get default route IfExists state if ($IfExists -ieq 'Default') { @@ -302,7 +303,8 @@ function Add-PodeRoute { # if middleware, scriptblock and file path are all null/empty, error if ((Test-PodeIsEmpty $Middleware) -and (Test-PodeIsEmpty $ScriptBlock) -and (Test-PodeIsEmpty $FilePath) -and (Test-PodeIsEmpty $Authentication)) { - throw "No logic passed for Route: $($Path)" + # [Method] Path: No logic passed + throw ($PodeLocale.noLogicPassedForMethodRouteExceptionMessage -f ($Method -join ','), $Path) } # if we have a file path supplied, load that path as a scriptblock @@ -319,11 +321,13 @@ function Add-PodeRoute { # if an access name was supplied, setup access as middleware first to it's after auth middleware if (![string]::IsNullOrWhiteSpace($Access)) { if ([string]::IsNullOrWhiteSpace($Authentication)) { - throw 'Access requires Authentication to be supplied on Routes' + # Access requires Authentication to be supplied on Routes + throw ($PodeLocale.accessRequiresAuthenticationOnRoutesExceptionMessage) } if (!(Test-PodeAccessExists -Name $Access)) { - throw "Access method does not exist: $($Access)" + # Access method does not exist + throw ($PodeLocale.accessMethodDoesNotExistExceptionMessage -f $Access) } $options = @{ @@ -336,7 +340,8 @@ function Add-PodeRoute { # if an auth name was supplied, setup the auth as the first middleware if (![string]::IsNullOrWhiteSpace($Authentication)) { if (!(Test-PodeAuthExists -Name $Authentication)) { - throw "Authentication method does not exist: $($Authentication)" + # Authentication method does not exist + throw ($PodeLocale.authenticationMethodDoesNotExistExceptionMessage -f $Authentication) } $options = @{ @@ -392,9 +397,9 @@ function Add-PodeRoute { #add the default OpenApi responses if ( $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.defaultResponses) { - $DefaultResponse = @{} + $DefaultResponse = [ordered]@{} foreach ($tag in $DefinitionTag) { - $DefaultResponse[$tag] = $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.defaultResponses.Clone() + $DefaultResponse[$tag] = Copy-PodeObjectDeepClone -InputObject $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.defaultResponses } } @@ -426,14 +431,15 @@ function Add-PodeRoute { Method = $_method Path = $Path OpenApi = @{ - Path = $OpenApiPath - Responses = $DefaultResponse - Parameters = $null - RequestBody = $null - CallBacks = @{} - Authentication = @() - Servers = @() - DefinitionTag = $DefinitionTag + Path = $OpenApiPath + Responses = $DefaultResponse + Parameters = $null + RequestBody = $null + CallBacks = @{} + Authentication = @() + Servers = @() + DefinitionTag = $DefinitionTag + IsDefTagConfigured = ($null -ne $OADefinitionTag) #Definition Tag has been configured (Not default) } IsStatic = $false Metrics = @{ @@ -730,16 +736,17 @@ function Add-PodeStaticRoute { # split route on '?' for query $Path = Split-PodeRouteQuery -Path $Path if ([string]::IsNullOrWhiteSpace($Path)) { - throw "[$($Method)]: No Path supplied for Static Route" + # No Path supplied for the Route. + throw ($PodeLocale.noPathSuppliedForRouteExceptionMessage) } # ensure the route has appropriate slashes - $Path = Update-PodeRouteSlashes -Path $Path -Static + $Path = Update-PodeRouteSlash -Path $Path -Static $OpenApiPath = ConvertTo-PodeOpenApiRoutePath -Path $Path - $Path = Resolve-PodePlaceholders -Path $Path + $Path = Resolve-PodePlaceholder -Path $Path # get endpoints from name - $endpoints = Find-PodeEndpoints -EndpointName $EndpointName + $endpoints = Find-PodeEndpoint -EndpointName $EndpointName # get default route IfExists state if ($IfExists -ieq 'Default') { @@ -770,7 +777,8 @@ function Add-PodeStaticRoute { # if static, ensure the path exists at server root $Source = Get-PodeRelativePath -Path $Source -JoinRoot if (!(Test-PodePath -Path $Source -NoStatus)) { - throw "[$($Method))] $($Path): The Source path supplied for Static Route does not exist: $($Source)" + # [Method)] Path: The Source path supplied for Static Route does not exist + throw ($PodeLocale.sourcePathDoesNotExistForStaticRouteExceptionMessage -f $Path, $Source) } # setup a temp drive for the path @@ -791,11 +799,13 @@ function Add-PodeStaticRoute { # if an access name was supplied, setup access as middleware first to it's after auth middleware if (![string]::IsNullOrWhiteSpace($Access)) { if ([string]::IsNullOrWhiteSpace($Authentication)) { - throw 'Access requires Authentication to be supplied on Static Routes' + # Access requires Authentication to be supplied on Routes + throw ($PodeLocale.accessRequiresAuthenticationOnRoutesExceptionMessage) } if (!(Test-PodeAccessExists -Name $Access)) { - throw "Access method does not exist: $($Access)" + # Access method does not exist + throw ($PodeLocale.accessMethodDoesNotExistExceptionMessage -f $Access) } $options = @{ @@ -808,7 +818,8 @@ function Add-PodeStaticRoute { # if an auth name was supplied, setup the auth as the first middleware if (![string]::IsNullOrWhiteSpace($Authentication)) { if (!(Test-PodeAuthExists -Name $Authentication)) { - throw "Authentication method does not exist: $($Authentication)" + # Authentication method does not exist + throw ($PodeLocale.authenticationMethodDoesNotExistExceptionMessage) } $options = @{ @@ -968,10 +979,10 @@ function Add-PodeSignalRoute { $origPath = $Path # ensure the route has appropriate slashes - $Path = Update-PodeRouteSlashes -Path $Path + $Path = Update-PodeRouteSlash -Path $Path # get endpoints from name - $endpoints = Find-PodeEndpoints -EndpointName $EndpointName + $endpoints = Find-PodeEndpoint -EndpointName $EndpointName # get default route IfExists state if ($IfExists -ieq 'Default') { @@ -1001,7 +1012,8 @@ function Add-PodeSignalRoute { # if scriptblock and file path are all null/empty, error if ((Test-PodeIsEmpty $ScriptBlock) -and (Test-PodeIsEmpty $FilePath)) { - throw "[$($Method)] $($Path): No logic passed" + # [Method] Path: No logic passed + throw ($PodeLocale.noLogicPassedForMethodRouteExceptionMessage -f $Method, $Path) } # if we have a file path supplied, load that path as a scriptblock @@ -1168,7 +1180,8 @@ function Add-PodeRouteGroup { ) if (Test-PodeIsEmpty $Routes) { - throw 'No scriptblock for -Routes passed' + # The Route parameter needs a valid, not empty, scriptblock + throw ($PodeLocale.routeParameterNeedsValidScriptblockExceptionMessage) } if ($Path -eq '/') { @@ -1425,7 +1438,8 @@ function Add-PodeStaticRouteGroup { ) if (Test-PodeIsEmpty $Routes) { - throw 'No scriptblock for -Routes passed' + # The Route parameter needs a valid, not empty, scriptblock + throw ($PodeLocale.routeParameterNeedsValidScriptblockExceptionMessage) } if ($Path -eq '/') { @@ -1592,7 +1606,8 @@ function Add-PodeSignalRouteGroup { ) if (Test-PodeIsEmpty $Routes) { - throw 'No scriptblock for -Routes passed' + # The Route parameter needs a valid, not empty, scriptblock + throw ($PodeLocale.routeParameterNeedsValidScriptblockExceptionMessage) } if ($Path -eq '/') { @@ -1668,24 +1683,28 @@ function Remove-PodeRoute { # split route on '?' for query $Path = Split-PodeRouteQuery -Path $Path - if ([string]::IsNullOrWhiteSpace($Path)) { - throw "[$($Method)]: No Route path supplied for removing a Route" - } # ensure the route has appropriate slashes and replace parameters - $Path = Update-PodeRouteSlashes -Path $Path - $Path = Resolve-PodePlaceholders -Path $Path + $Path = Update-PodeRouteSlash -Path $Path + $Path = Resolve-PodePlaceholder -Path $Path # ensure route does exist if (!$PodeContext.Server.Routes[$Method].Contains($Path)) { return } - # remove the operationId from the openapi operationId list - if ($PodeContext.Server.Routes[$Method][$Path].OpenAPI) { - foreach ( $tag in $PodeContext.Server.Routes[$Method][$Path].OpenAPI.DefinitionTag) { - if ($tag -and ($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId -ccontains $PodeContext.Server.Routes[$Method][$Path].OpenAPI.OperationId)) { - $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId = $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId | Where-Object { $_ -ne $PodeContext.Server.Routes[$Method][$Path].OpenAPI.OperationId } + # select the candidate route for deletion + $route = @($PodeContext.Server.Routes[$Method][$Path] | Where-Object { + $_.Endpoint.Name -ieq $EndpointName + }) + + foreach ($r in $route) { + # remove the operationId from the openapi operationId list + if ($r.OpenAPI) { + foreach ( $tag in $r.OpenAPI.DefinitionTag) { + if ($tag -and ($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId -ccontains $route.OpenAPI.OperationId)) { + $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId = $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId | Where-Object { $_ -ne $route.OpenAPI.OperationId } + } } } } @@ -1732,7 +1751,7 @@ function Remove-PodeStaticRoute { $Method = 'Static' # ensure the route has appropriate slashes and replace parameters - $Path = Update-PodeRouteSlashes -Path $Path -Static + $Path = Update-PodeRouteSlash -Path $Path -Static # ensure route does exist if (!$PodeContext.Server.Routes[$Method].Contains($Path)) { @@ -1781,7 +1800,7 @@ function Remove-PodeSignalRoute { $Method = 'Signal' # ensure the route has appropriate slashes and replace parameters - $Path = Update-PodeRouteSlashes -Path $Path + $Path = Update-PodeRouteSlash -Path $Path # ensure route does exist if (!$PodeContext.Server.Routes[$Method].Contains($Path)) { @@ -1816,6 +1835,7 @@ Clear-PodeRoutes Clear-PodeRoutes -Method Get #> function Clear-PodeRoutes { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param( [Parameter()] @@ -1845,6 +1865,7 @@ Removes all added static Routes. Clear-PodeStaticRoutes #> function Clear-PodeStaticRoutes { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param() @@ -1862,6 +1883,7 @@ Removes all added Signal Routes. Clear-PodeSignalRoutes #> function Clear-PodeSignalRoutes { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param() @@ -1932,7 +1954,7 @@ ConvertTo-PodeRoute -Commands @('Invoke-Pester') -Module Pester function ConvertTo-PodeRoute { [CmdletBinding()] param( - [Parameter(ValueFromPipeline = $true)] + [Parameter(ValueFromPipeline = $true, Position = 0 )] [string[]] $Commands, @@ -1987,129 +2009,147 @@ function ConvertTo-PodeRoute { [switch] $NoOpenApi ) - - # if a module was supplied, import it - then validate the commands - if (![string]::IsNullOrWhiteSpace($Module)) { - Import-PodeModule -Name $Module - - Write-Verbose 'Getting exported commands from module' - $ModuleCommands = (Get-Module -Name $Module | Sort-Object -Descending | Select-Object -First 1).ExportedCommands.Keys - - # if commands were supplied validate them - otherwise use all exported ones - if (Test-PodeIsEmpty $Commands) { - Write-Verbose "Using all commands in $($Module) for converting to routes" - $Commands = $ModuleCommands - } - else { - Write-Verbose "Validating supplied commands against module's exported commands" - foreach ($cmd in $Commands) { - if ($ModuleCommands -inotcontains $cmd) { - throw "Module $($Module) does not contain function $($cmd) to convert to a Route" - } - } - } + begin { + # Initialize an array to hold piped-in values + $pipelineValue = @() } - # if there are no commands, fail - if (Test-PodeIsEmpty $Commands) { - throw 'No commands supplied to convert to Routes' + process { + # Add the current piped-in value to the array + $pipelineValue += $_ } - # trim end trailing slashes from the path - $Path = Protect-PodeValue -Value $Path -Default '/' - $Path = $Path.TrimEnd('/') + end { + # Set InputObject to the array of values + if ($pipelineValue.Count -gt 1) { + $Commands = $pipelineValue + } - # create the routes for each of the commands - foreach ($cmd in $Commands) { - # get module verb/noun and comvert verb to HTTP method - $split = ($cmd -split '\-') + # if a module was supplied, import it - then validate the commands + if (![string]::IsNullOrWhiteSpace($Module)) { + Import-PodeModule -Name $Module - if ($split.Length -ge 2) { - $verb = $split[0] - $noun = $split[1..($split.Length - 1)] -join ([string]::Empty) - } - else { - $verb = [string]::Empty - $noun = $split[0] - } + Write-Verbose 'Getting exported commands from module' + $ModuleCommands = (Get-Module -Name $Module | Sort-Object -Descending | Select-Object -First 1).ExportedCommands.Keys - # determine the http method, or use the one passed - $_method = $Method - if ([string]::IsNullOrWhiteSpace($_method)) { - $_method = Convert-PodeFunctionVerbToHttpMethod -Verb $verb + # if commands were supplied validate them - otherwise use all exported ones + if (Test-PodeIsEmpty $Commands) { + Write-Verbose "Using all commands in $($Module) for converting to routes" + $Commands = $ModuleCommands + } + else { + Write-Verbose "Validating supplied commands against module's exported commands" + foreach ($cmd in $Commands) { + if ($ModuleCommands -inotcontains $cmd) { + # Module Module does not contain function cmd to convert to a Route + throw ($PodeLocale.moduleDoesNotContainFunctionExceptionMessage -f $Module, $cmd) + } + } + } } - # use the full function name, or remove the verb - $name = $cmd - if ($NoVerb) { - $name = $noun + # if there are no commands, fail + if (Test-PodeIsEmpty $Commands) { + # No commands supplied to convert to Routes + throw ($PodeLocale.noCommandsSuppliedToConvertToRoutesExceptionMessage) } - # build the route's path - $_path = ("$($Path)/$($Module)/$($name)" -replace '[/]+', '/') + # trim end trailing slashes from the path + $Path = Protect-PodeValue -Value $Path -Default '/' + $Path = $Path.TrimEnd('/') - # create the route - $params = @{ - Method = $_method - Path = $_path - Middleware = $Middleware - Authentication = $Authentication - Access = $Access - Role = $Role - Group = $Group - Scope = $Scope - User = $User - AllowAnon = $AllowAnon - ArgumentList = $cmd - PassThru = $true - } - - $route = Add-PodeRoute @params -ScriptBlock { - param($cmd) + # create the routes for each of the commands + foreach ($cmd in $Commands) { + # get module verb/noun and comvert verb to HTTP method + $split = ($cmd -split '\-') - # either get params from the QueryString or Payload - if ($WebEvent.Method -ieq 'get') { - $parameters = $WebEvent.Query + if ($split.Length -ge 2) { + $verb = $split[0] + $noun = $split[1..($split.Length - 1)] -join ([string]::Empty) } else { - $parameters = $WebEvent.Data + $verb = [string]::Empty + $noun = $split[0] + } + + # determine the http method, or use the one passed + $_method = $Method + if ([string]::IsNullOrWhiteSpace($_method)) { + $_method = Convert-PodeFunctionVerbToHttpMethod -Verb $verb } - # invoke the function - $result = (. $cmd @parameters) + # use the full function name, or remove the verb + $name = $cmd + if ($NoVerb) { + $name = $noun + } - # if we have a result, convert it to json - if (!(Test-PodeIsEmpty $result)) { - Write-PodeJsonResponse -Value $result -Depth 1 + # build the route's path + $_path = ("$($Path)/$($Module)/$($name)" -replace '[/]+', '/') + + # create the route + $params = @{ + Method = $_method + Path = $_path + Middleware = $Middleware + Authentication = $Authentication + Access = $Access + Role = $Role + Group = $Group + Scope = $Scope + User = $User + AllowAnon = $AllowAnon + ArgumentList = $cmd + PassThru = $true } - } - # set the openapi metadata of the function, unless told to skip - if ($NoOpenApi) { - continue - } + $route = Add-PodeRoute @params -ScriptBlock { + param($cmd) - $help = Get-Help -Name $cmd - $route = ($route | Set-PodeOARouteInfo -Summary $help.Synopsis -Tags $Module -PassThru) + # either get params from the QueryString or Payload + if ($WebEvent.Method -ieq 'get') { + $parameters = $WebEvent.Query + } + else { + $parameters = $WebEvent.Data + } - # set the routes parameters (get = query, everything else = payload) - $params = (Get-Command -Name $cmd).Parameters - if (($null -eq $params) -or ($params.Count -eq 0)) { - continue - } + # invoke the function + $result = (. $cmd @parameters) - $props = @(foreach ($key in $params.Keys) { - $params[$key] | ConvertTo-PodeOAPropertyFromCmdletParameter - }) + # if we have a result, convert it to json + if (!(Test-PodeIsEmpty $result)) { + Write-PodeJsonResponse -Value $result -Depth 1 + } + } - if ($_method -ieq 'get') { - $route | Set-PodeOARequest -Parameters @(foreach ($prop in $props) { $prop | ConvertTo-PodeOAParameter -In Query }) - } + # set the openapi metadata of the function, unless told to skip + if ($NoOpenApi) { + continue + } - else { - $route | Set-PodeOARequest -RequestBody ( - New-PodeOARequestBody -ContentSchemas @{ 'application/json' = (New-PodeOAObjectProperty -Array -Properties $props) } - ) + $help = Get-Help -Name $cmd + $route = ($route | Set-PodeOARouteInfo -Summary $help.Synopsis -Tags $Module -PassThru) + + # set the routes parameters (get = query, everything else = payload) + $params = (Get-Command -Name $cmd).Parameters + if (($null -eq $params) -or ($params.Count -eq 0)) { + continue + } + + $props = @(foreach ($key in $params.Keys) { + $params[$key] | ConvertTo-PodeOAPropertyFromCmdletParameter + }) + + if ($_method -ieq 'get') { + $route | Set-PodeOARequest -Parameters @(foreach ($prop in $props) { $prop | ConvertTo-PodeOAParameter -In Query }) + } + + else { + $route | Set-PodeOARequest -RequestBody ( + New-PodeOARequestBody -ContentSchemas @{ 'application/json' = (New-PodeOAObjectProperty -Array -Properties $props) } + ) + } } } } @@ -2246,7 +2286,8 @@ function Add-PodePage { # ensure the name is a valid alphanumeric if ($Name -inotmatch '^[a-z0-9\-_]+$') { - throw "The Page name should be a valid AlphaNumeric value: $($Name)" + # The Page name should be a valid AlphaNumeric value + throw ($PodeLocale.pageNameShouldBeAlphaNumericExceptionMessage -f $Name) } # trim end trailing slashes from the path @@ -2257,7 +2298,8 @@ function Add-PodePage { switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { 'scriptblock' { if (Test-PodeIsEmpty $ScriptBlock) { - throw 'A non-empty ScriptBlock is required to created a Page Route' + # A non-empty ScriptBlock is required to create a Page Route + throw ($PodeLocale.nonEmptyScriptBlockRequiredForPageRouteExceptionMessage) } $arg = @($ScriptBlock, $Data) @@ -2343,6 +2385,7 @@ Get-PodeRoute -Method Post -Path '/users/:userId' -EndpointName User #> function Get-PodeRoute { [CmdletBinding()] + [OutputType([System.Object[]])] param( [Parameter()] [ValidateSet('', 'Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')] @@ -2378,8 +2421,8 @@ function Get-PodeRoute { # if we have a path, filter if (![string]::IsNullOrWhiteSpace($Path)) { $Path = Split-PodeRouteQuery -Path $Path - $Path = Update-PodeRouteSlashes -Path $Path - $Path = Resolve-PodePlaceholders -Path $Path + $Path = Update-PodeRouteSlash -Path $Path + $Path = Resolve-PodePlaceholder -Path $Path $routes = @(foreach ($route in $routes) { if ($route.Path -ine $Path) { @@ -2428,6 +2471,7 @@ Get-PodeStaticRoute -Path '/assets' -EndpointName User #> function Get-PodeStaticRoute { [CmdletBinding()] + [OutputType([System.Object[]])] param( [Parameter()] [string] @@ -2446,7 +2490,7 @@ function Get-PodeStaticRoute { # if we have a path, filter if (![string]::IsNullOrWhiteSpace($Path)) { - $Path = Update-PodeRouteSlashes -Path $Path -Static + $Path = Update-PodeRouteSlash -Path $Path -Static $routes = @(foreach ($route in $routes) { if ($route.Path -ine $Path) { continue @@ -2491,6 +2535,7 @@ Get-PodeSignalRoute -Path '/message' #> function Get-PodeSignalRoute { [CmdletBinding()] + [OutputType([System.Object[]])] param( [Parameter()] [string] @@ -2509,7 +2554,7 @@ function Get-PodeSignalRoute { # if we have a path, filter if (![string]::IsNullOrWhiteSpace($Path)) { - $Path = Update-PodeRouteSlashes -Path $Path + $Path = Update-PodeRouteSlash -Path $Path $routes = @(foreach ($route in $routes) { if ($route.Path -ine $Path) { continue @@ -2556,6 +2601,7 @@ Use-PodeRoutes Use-PodeRoutes -Path './my-routes' -IfExists Skip #> function Use-PodeRoutes { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param( [Parameter()] @@ -2651,15 +2697,16 @@ function Test-PodeRoute { # split route on '?' for query $Path = Split-PodeRouteQuery -Path $Path if ([string]::IsNullOrWhiteSpace($Path)) { - throw 'No Path supplied for testing Route' + # No Path supplied for the Route + throw ($PodeLocale.noPathSuppliedForRouteExceptionMessage) } # ensure the route has appropriate slashes - $Path = Update-PodeRouteSlashes -Path $Path - $Path = Resolve-PodePlaceholders -Path $Path + $Path = Update-PodeRouteSlash -Path $Path + $Path = Resolve-PodePlaceholder -Path $Path # get endpoint from name - $endpoint = @(Find-PodeEndpoints -EndpointName $EndpointName)[0] + $endpoint = @(Find-PodeEndpoint -EndpointName $EndpointName)[0] # check for routes $found = (Test-PodeRouteInternal -Method $Method -Path $Path -Protocol $endpoint.Protocol -Address $endpoint.Address) @@ -2704,15 +2751,16 @@ function Test-PodeStaticRoute { # split route on '?' for query $Path = Split-PodeRouteQuery -Path $Path if ([string]::IsNullOrWhiteSpace($Path)) { - throw 'No Path supplied for testing Static Route' + # No Path supplied for the Route + throw ($PodeLocale.noPathSuppliedForRouteExceptionMessage) } # ensure the route has appropriate slashes - $Path = Update-PodeRouteSlashes -Path $Path -Static - $Path = Resolve-PodePlaceholders -Path $Path + $Path = Update-PodeRouteSlash -Path $Path -Static + $Path = Resolve-PodePlaceholder -Path $Path # get endpoint from name - $endpoint = @(Find-PodeEndpoints -EndpointName $EndpointName)[0] + $endpoint = @(Find-PodeEndpoint -EndpointName $EndpointName)[0] # check for routes return (Test-PodeRouteInternal -Method $Method -Path $Path -Protocol $endpoint.Protocol -Address $endpoint.Address) @@ -2749,10 +2797,10 @@ function Test-PodeSignalRoute { $Method = 'Signal' # ensure the route has appropriate slashes - $Path = Update-PodeRouteSlashes -Path $Path + $Path = Update-PodeRouteSlash -Path $Path # get endpoint from name - $endpoint = @(Find-PodeEndpoints -EndpointName $EndpointName)[0] + $endpoint = @(Find-PodeEndpoint -EndpointName $EndpointName)[0] # check for routes return (Test-PodeRouteInternal -Method $Method -Path $Path -Protocol $endpoint.Protocol -Address $endpoint.Address) diff --git a/src/Public/Runspaces.ps1 b/src/Public/Runspaces.ps1 new file mode 100644 index 000000000..286a0aacd --- /dev/null +++ b/src/Public/Runspaces.ps1 @@ -0,0 +1,61 @@ +<# +.SYNOPSIS + Sets the name of the current runspace. + +.DESCRIPTION + The Set-PodeCurrentRunspaceName function assigns a specified name to the current runspace. + This can be useful for identifying and managing the runspace in scripts and during debugging. + +.PARAMETER Name + The name to assign to the current runspace. This parameter is mandatory. + +.EXAMPLE + Set-PodeCurrentRunspaceName -Name "MyRunspace" + This command sets the name of the current runspace to "MyRunspace". + +.NOTES + This is an internal function and may change in future releases of Pode. +#> + +function Set-PodeCurrentRunspaceName { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + # Get the current runspace + $currentRunspace = [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace + # Set the name of the current runspace if the name is not already set + if ( $currentRunspace.Name -ne $Name) { + # Set the name of the current runspace + $currentRunspace.Name = $Name + } +} + +<# +.SYNOPSIS + Retrieves the name of the current PowerShell runspace. + +.DESCRIPTION + The Get-PodeCurrentRunspaceName function retrieves the name of the current PowerShell runspace. + This can be useful for debugging or logging purposes to identify the runspace in use. + +.EXAMPLE + Get-PodeCurrentRunspaceName + Returns the name of the current runspace. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Get-PodeCurrentRunspaceName { + [CmdletBinding()] + param() + + # Get the current runspace + $currentRunspace = [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace + + # Get the name of the current runspace + return $currentRunspace.Name +} diff --git a/src/Public/SSE.ps1 b/src/Public/SSE.ps1 index c3203e8a4..c1d620139 100644 --- a/src/Public/SSE.ps1 +++ b/src/Public/SSE.ps1 @@ -78,7 +78,8 @@ function ConvertTo-PodeSseConnection { # check Accept header - unless forcing if (!$Force -and ((Get-PodeHeader -Name 'Accept') -ine 'text/event-stream')) { - throw 'SSE can only be configured on requests with an Accept header value of text/event-stream' + # SSE can only be configured on requests with an Accept header value of text/event-stream + throw ($PodeLocale.sseOnlyConfiguredOnEventStreamAcceptHeaderExceptionMessage) } # check for default scope, and set @@ -90,7 +91,7 @@ function ConvertTo-PodeSseConnection { $ClientId = New-PodeSseClientId -ClientId $ClientId # set and send SSE headers - $ClientId = $WebEvent.Response.SetSseConnection($Scope, $ClientId, $Name, $Group, $RetryDuration, $AllowAllOrigins.IsPresent) + $ClientId = Wait-PodeTask -Task $WebEvent.Response.SetSseConnection($Scope, $ClientId, $Name, $Group, $RetryDuration, $AllowAllOrigins.IsPresent) # create SSE property on WebEvent $WebEvent.Sse = @{ @@ -200,8 +201,11 @@ Send-PodeSseEvent -Name 'Actions' -Group 'admins' -Data @{ Message = 'A message' Send-PodeSseEvent -Name 'Actions' -Data @{ Message = 'A message' } -ID 123 -EventType 'action' #> function Send-PodeSseEvent { - [CmdletBinding()] + [CmdletBinding(DefaultParameterSetName = 'WebEvent')] param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] + $Data, + [Parameter(Mandatory = $true, ParameterSetName = 'Name')] [string] $Name, @@ -222,9 +226,6 @@ function Send-PodeSseEvent { [string] $EventType, - [Parameter(Mandatory = $true)] - $Data, - [Parameter()] [int] $Depth = 10, @@ -234,48 +235,62 @@ function Send-PodeSseEvent { $FromEvent ) - # do nothing if no value - if (($null -eq $Data) -or ([string]::IsNullOrEmpty($Data))) { - return - } - # jsonify the value - if ($Data -isnot [string]) { - if ($Depth -le 0) { - $Data = (ConvertTo-Json -InputObject $Data -Compress) - } - else { - $Data = (ConvertTo-Json -InputObject $Data -Depth $Depth -Compress) + begin { + $pipelineValue = @() + # do nothing if no value + if (($null -eq $Data) -or ([string]::IsNullOrEmpty($Data))) { + return } } - # send directly back to current connection - if ($FromEvent -and $WebEvent.Sse.IsLocal) { - $WebEvent.Response.SendSseEvent($EventType, $Data, $Id) - return + process { + $pipelineValue += $_ } - # from event and global? - if ($FromEvent) { - $Name = $WebEvent.Sse.Name - $Group = $WebEvent.Sse.Group - $ClientId = $WebEvent.Sse.ClientId - } + end { + if ($pipelineValue.Count -gt 1) { + $Data = $pipelineValue + } + # jsonify the value + if ($Data -isnot [string]) { + if ($Depth -le 0) { + $Data = (ConvertTo-Json -InputObject $Data -Compress) + } + else { + $Data = (ConvertTo-Json -InputObject $Data -Depth $Depth -Compress) + } + } - # error if no name - if ([string]::IsNullOrEmpty($Name)) { - throw 'An SSE connection Name is required, either from -Name or $WebEvent.Sse.Name' - } + # send directly back to current connection + if ($FromEvent -and $WebEvent.Sse.IsLocal) { + $null = Wait-PodeTask -Task $WebEvent.Response.SendSseEvent($EventType, $Data, $Id) + return + } - # check if broadcast level - if (!(Test-PodeSseBroadcastLevel -Name $Name -Group $Group -ClientId $ClientId)) { - throw "SSE failed to broadcast due to defined SSE broadcast level for $($Name): $(Get-PodeSseBroadcastLevel -Name $Name)" - } + # from event and global? + if ($FromEvent) { + $Name = $WebEvent.Sse.Name + $Group = $WebEvent.Sse.Group + $ClientId = $WebEvent.Sse.ClientId + } - # send event - $PodeContext.Server.Http.Listener.SendSseEvent($Name, $Group, $ClientId, $EventType, $Data, $Id) -} + # error if no name + if ([string]::IsNullOrEmpty($Name)) { + # An SSE connection Name is required, either from -Name or $WebEvent.Sse.Name + throw ($PodeLocale.sseConnectionNameRequiredExceptionMessage) + } + + # check if broadcast level + if (!(Test-PodeSseBroadcastLevel -Name $Name -Group $Group -ClientId $ClientId)) { + # SSE failed to broadcast due to defined SSE broadcast level + throw ($PodeLocale.sseFailedToBroadcastExceptionMessage -f $Name, (Get-PodeSseBroadcastLevel -Name $Name)) + } + # send event + $PodeContext.Server.Http.Listener.SendSseEvent($Name, $Group, $ClientId, $EventType, $Data, $Id) + } +} <# .SYNOPSIS Close one or more SSE connections. @@ -373,6 +388,7 @@ if (Test-PodeSseClientIdValid -ClientId 'my-client-id') { ... } #> function Test-PodeSseClientIdValid { [CmdletBinding()] + [OutputType([bool])] param( [Parameter()] [string] @@ -614,6 +630,7 @@ $level = Get-PodeSseBroadcastLevel -Name 'Actions' #> function Get-PodeSseBroadcastLevel { [CmdletBinding()] + [OutputType([string])] param( [Parameter(Mandatory = $true)] [string] @@ -666,6 +683,7 @@ if (Test-PodeSseBroadcastLevel -Name 'Actions' -ClientId 'my-client-id') { ... } #> function Test-PodeSseBroadcastLevel { [CmdletBinding()] + [OutputType([bool])] param( [Parameter(Mandatory = $true)] [string] diff --git a/src/Public/Schedules.ps1 b/src/Public/Schedules.ps1 index e88b577c4..974a0341c 100644 --- a/src/Public/Schedules.ps1 +++ b/src/Public/Schedules.ps1 @@ -1,48 +1,54 @@ <# .SYNOPSIS -Adds a new Schedule with logic to periodically invoke, defined using Cron Expressions. + Adds a new Schedule with logic to periodically invoke, defined using Cron Expressions. .DESCRIPTION -Adds a new Schedule with logic to periodically invoke, defined using Cron Expressions. + Adds a new Schedule with logic to periodically invoke, defined using Cron Expressions. .PARAMETER Name -The Name of the Schedule. + The Name of the Schedule. .PARAMETER Cron -One, or an Array, of Cron Expressions to define when the Schedule should trigger. + One, or an Array, of Cron Expressions to define when the Schedule should trigger. .PARAMETER ScriptBlock -The script defining the Schedule's logic. + The script defining the Schedule's logic. .PARAMETER Limit -The number of times the Schedule should trigger before being removed. + The number of times the Schedule should trigger before being removed. .PARAMETER StartTime -A DateTime for when the Schedule should start triggering. + A DateTime for when the Schedule should start triggering. .PARAMETER EndTime -A DateTime for when the Schedule should stop triggering, and be removed. + A DateTime for when the Schedule should stop triggering, and be removed. .PARAMETER ArgumentList -A hashtable of arguments to supply to the Schedule's ScriptBlock. + A hashtable of arguments to supply to the Schedule's ScriptBlock. + +.PARAMETER Timeout + An optional timeout, in seconds, for the Schedule's logic. (Default: -1 [never timeout]) + +.PARAMETER TimeoutFrom + An optional timeout from either 'Create' or 'Start'. (Default: 'Create') .PARAMETER FilePath -A literal, or relative, path to a file containing a ScriptBlock for the Schedule's logic. + A literal, or relative, path to a file containing a ScriptBlock for the Schedule's logic. .PARAMETER OnStart -If supplied, the schedule will trigger when the server starts, regardless if the cron-expression matches the current time. + If supplied, the schedule will trigger when the server starts, regardless if the cron-expression matches the current time. .EXAMPLE -Add-PodeSchedule -Name 'RunEveryMinute' -Cron '@minutely' -ScriptBlock { /* logic */ } + Add-PodeSchedule -Name 'RunEveryMinute' -Cron '@minutely' -ScriptBlock { /* logic */ } .EXAMPLE -Add-PodeSchedule -Name 'RunEveryTuesday' -Cron '0 0 * * TUE' -ScriptBlock { /* logic */ } + Add-PodeSchedule -Name 'RunEveryTuesday' -Cron '0 0 * * TUE' -ScriptBlock { /* logic */ } .EXAMPLE -Add-PodeSchedule -Name 'StartAfter2days' -Cron '@hourly' -StartTime [DateTime]::Now.AddDays(2) -ScriptBlock { /* logic */ } + Add-PodeSchedule -Name 'StartAfter2days' -Cron '@hourly' -StartTime [DateTime]::Now.AddDays(2) -ScriptBlock { /* logic */ } .EXAMPLE -Add-PodeSchedule -Name 'Args' -Cron '@minutely' -ScriptBlock { /* logic */ } -ArgumentList @{ Arg1 = 'value' } + Add-PodeSchedule -Name 'Args' -Cron '@minutely' -ScriptBlock { /* logic */ } -ArgumentList @{ Arg1 = 'value' } #> function Add-PodeSchedule { [CmdletBinding(DefaultParameterSetName = 'Script')] @@ -79,6 +85,15 @@ function Add-PodeSchedule { [hashtable] $ArgumentList, + [Parameter()] + [int] + $Timeout = -1, + + [Parameter()] + [ValidateSet('Create', 'Start')] + [string] + $TimeoutFrom = 'Create', + [switch] $OnStart ) @@ -88,21 +103,25 @@ function Add-PodeSchedule { # ensure the schedule doesn't already exist if ($PodeContext.Schedules.Items.ContainsKey($Name)) { - throw "[Schedule] $($Name): Schedule already defined" + # [Schedule] Name: Schedule already defined + throw ($PodeLocale.scheduleAlreadyDefinedExceptionMessage -f $Name) } # ensure the limit is valid if ($Limit -lt 0) { - throw "[Schedule] $($Name): Cannot have a negative limit" + # [Schedule] Name: Cannot have a negative limit + throw ($PodeLocale.scheduleCannotHaveNegativeLimitExceptionMessage -f $Name) } # ensure the start/end dates are valid if (($null -ne $EndTime) -and ($EndTime -lt [DateTime]::Now)) { - throw "[Schedule] $($Name): The EndTime value must be in the future" + # [Schedule] Name: The EndTime value must be in the future + throw ($PodeLocale.scheduleEndTimeMustBeInFutureExceptionMessage -f $Name) } if (($null -ne $StartTime) -and ($null -ne $EndTime) -and ($EndTime -le $StartTime)) { - throw "[Schedule] $($Name): Cannot have a StartTime after the EndTime" + # [Schedule] Name: Cannot have a 'StartTime' after the 'EndTime' + throw ($PodeLocale.scheduleStartTimeAfterEndTimeExceptionMessage -f $Name) } # if we have a file path supplied, load that path as a scriptblock @@ -114,7 +133,7 @@ function Add-PodeSchedule { $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState # add the schedule - $parsedCrons = ConvertFrom-PodeCronExpressions -Expressions @($Cron) + $parsedCrons = ConvertFrom-PodeCronExpression -Expression @($Cron) $nextTrigger = Get-PodeCronNextEarliestTrigger -Expressions $parsedCrons -StartTime $StartTime -EndTime $EndTime $PodeContext.Schedules.Enabled = $true @@ -133,6 +152,10 @@ function Add-PodeSchedule { Arguments = (Protect-PodeValue -Value $ArgumentList -Default @{}) OnStart = $OnStart Completed = ($null -eq $nextTrigger) + Timeout = @{ + Value = $Timeout + From = $TimeoutFrom + } } } @@ -159,7 +182,8 @@ function Set-PodeScheduleConcurrency { # error if <=0 if ($Maximum -le 0) { - throw "Maximum concurrent schedules must be >=1 but got: $($Maximum)" + # Maximum concurrent schedules must be >=1 but got + throw ($PodeLocale.maximumConcurrentSchedulesInvalidExceptionMessage -f $Maximum) } # ensure max > min @@ -169,7 +193,8 @@ function Set-PodeScheduleConcurrency { } if ($_min -gt $Maximum) { - throw "Maximum concurrent schedules cannot be less than the minimum of $($_min) but got: $($Maximum)" + # Maximum concurrent schedules cannot be less than the minimum of $_min but got $Maximum + throw ($PodeLocale.maximumConcurrentSchedulesLessThanMinimumExceptionMessage -f $_min, $Maximum) } # set the max schedules @@ -209,7 +234,8 @@ function Invoke-PodeSchedule { # ensure the schedule exists if (!$PodeContext.Schedules.Items.ContainsKey($Name)) { - throw "Schedule '$($Name)' does not exist" + # Schedule 'Name' does not exist + throw ($PodeLocale.scheduleDoesNotExistExceptionMessage -f $Name) } # run schedule logic @@ -251,6 +277,7 @@ Removes all Schedules. Clear-PodeSchedules #> function Clear-PodeSchedules { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param() @@ -304,14 +331,15 @@ function Edit-PodeSchedule { # ensure the schedule exists if (!$PodeContext.Schedules.Items.ContainsKey($Name)) { - throw "Schedule '$($Name)' does not exist" + # Schedule 'Name' does not exist + throw ($PodeLocale.scheduleDoesNotExistExceptionMessage -f $Name) } $_schedule = $PodeContext.Schedules.Items[$Name] # edit cron if supplied if (!(Test-PodeIsEmpty $Cron)) { - $_schedule.Crons = (ConvertFrom-PodeCronExpressions -Expressions @($Cron)) + $_schedule.Crons = (ConvertFrom-PodeCronExpression -Expression @($Cron)) $_schedule.CronsRaw = $Cron $_schedule.NextTriggerTime = Get-PodeCronNextEarliestTrigger -Expressions $_schedule.Crons -StartTime $_schedule.StartTime -EndTime $_schedule.EndTime } @@ -487,18 +515,21 @@ function Get-PodeScheduleNextTrigger { # ensure the schedule exists if (!$PodeContext.Schedules.Items.ContainsKey($Name)) { - throw "Schedule '$($Name)' does not exist" + # Schedule 'Name' does not exist + throw ($PodeLocale.scheduleDoesNotExistExceptionMessage -f $Name) } $_schedule = $PodeContext.Schedules.Items[$Name] # ensure date is after start/before end if (($null -ne $DateTime) -and ($null -ne $_schedule.StartTime) -and ($DateTime -lt $_schedule.StartTime)) { - throw "Supplied date is before the start time of the schedule at $($_schedule.StartTime)" + # Supplied date is before the start time of the schedule at $_schedule.StartTime + throw ($PodeLocale.suppliedDateBeforeScheduleStartTimeExceptionMessage -f $_schedule.StartTime) } if (($null -ne $DateTime) -and ($null -ne $_schedule.EndTime) -and ($DateTime -gt $_schedule.EndTime)) { - throw "Supplied date is after the end time of the schedule at $($_schedule.EndTime)" + # Supplied date is after the end time of the schedule at $_schedule.EndTime + throw ($PodeLocale.suppliedDateAfterScheduleEndTimeExceptionMessage -f $_schedule.EndTime) } # get the next trigger @@ -526,6 +557,7 @@ Use-PodeSchedules Use-PodeSchedules -Path './my-schedules' #> function Use-PodeSchedules { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param( [Parameter()] @@ -534,4 +566,92 @@ function Use-PodeSchedules { ) Use-PodeFolder -Path $Path -DefaultPath 'schedules' +} + +<# +.SYNOPSIS +Get all Schedule Processes. + +.DESCRIPTION +Get all Schedule Processes, with support for filtering. + +.PARAMETER Name +An optional Name of the Schedule to filter by, can be one or more. + +.PARAMETER Id +An optional ID of the Schedule process to filter by, can be one or more. + +.PARAMETER State +An optional State of the Schedule process to filter by, can be one or more. + +.EXAMPLE +Get-PodeScheduleProcess + +.EXAMPLE +Get-PodeScheduleProcess -Name 'ScheduleName' + +.EXAMPLE +Get-PodeScheduleProcess -Id 'ScheduleId' + +.EXAMPLE +Get-PodeScheduleProcess -State 'Running' +#> +function Get-PodeScheduleProcess { + [CmdletBinding()] + param( + [Parameter()] + [string[]] + $Name, + + [Parameter()] + [string[]] + $Id, + + [Parameter()] + [ValidateSet('All', 'Pending', 'Running', 'Completed', 'Failed')] + [string[]] + $State = 'All' + ) + + $processes = $PodeContext.Schedules.Processes.Values + + # filter processes by name + if (($null -ne $Name) -and ($Name.Length -gt 0)) { + $processes = @(foreach ($_name in $Name) { + foreach ($process in $processes) { + if ($process.Schedule -ine $_name) { + continue + } + + $process + } + }) + } + + # filter processes by id + if (($null -ne $Id) -and ($Id.Length -gt 0)) { + $processes = @(foreach ($_id in $Id) { + foreach ($process in $processes) { + if ($process.ID -ine $_id) { + continue + } + + $process + } + }) + } + + # filter processes by status + if ($State -inotcontains 'All') { + $processes = @(foreach ($process in $processes) { + if ($State -inotcontains $process.State) { + continue + } + + $process + }) + } + + # return processes + return $processes } \ No newline at end of file diff --git a/src/Public/ScopedVariables.ps1 b/src/Public/ScopedVariables.ps1 index 35ade7d2a..a21805c13 100644 --- a/src/Public/ScopedVariables.ps1 +++ b/src/Public/ScopedVariables.ps1 @@ -23,7 +23,10 @@ $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock $ScriptBlock = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -Exclude Session, Using #> function Convert-PodeScopedVariables { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] + [OutputType([System.Object[]])] + [OutputType([scriptblock])] param( [Parameter(ValueFromPipeline = $true)] [scriptblock] @@ -95,6 +98,7 @@ $ScriptBlock, $otherResults = Convert-PodeScopedVariable -Name Using -ScriptBloc #> function Convert-PodeScopedVariable { [CmdletBinding()] + [OutputType([scriptblock])] param( [Parameter(Mandatory = $true)] [string] @@ -116,7 +120,8 @@ function Convert-PodeScopedVariable { # check if scoped var defined if (!(Test-PodeScopedVariable -Name $Name)) { - throw "Scoped Variable not found: $($Name)" + # Scoped Variable not found + throw ($PodeLocale.scopedVariableNotFoundExceptionMessage -f $Name) } # get the scoped var metadata @@ -297,6 +302,10 @@ Removes all Scoped Variables. Clear-PodeScopedVariables #> function Clear-PodeScopedVariables { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] + [CmdletBinding()] + param() + $null = $PodeContext.Server.ScopedVariables.Clear() } @@ -318,6 +327,7 @@ Get-PodeScopedVariable -Name State, Using #> function Get-PodeScopedVariable { [CmdletBinding()] + [OutputType([System.Object[]])] param( [Parameter()] [string[]] @@ -352,6 +362,7 @@ Use-PodeScopedVariables Use-PodeScopedVariables -Path './my-vars' #> function Use-PodeScopedVariables { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param( [Parameter()] diff --git a/src/Public/Secrets.ps1 b/src/Public/Secrets.ps1 index 82360ab50..782f659cf 100644 --- a/src/Public/Secrets.ps1 +++ b/src/Public/Secrets.ps1 @@ -130,8 +130,8 @@ function Register-PodeSecretVault { if ($PodeContext.Server.Secrets.Vaults[$Name].AutoImported) { $autoImported = ' from auto-importing' } - - throw "A Secret Vault with the name '$($Name)' has already been registered$($autoImported)" + # A Secret Vault with the name {0} has already been registered{1} + throw ($PodeLocale.secretVaultAlreadyRegisteredAutoImportExceptionMessage -f $Name, $autoImported) } # base vault config @@ -261,7 +261,8 @@ function Unlock-PodeSecretVault { # has the vault been registered? if (!(Test-PodeSecretVault -Name $Name)) { - throw "No Secret Vault with the name '$($Name)' has been registered" + # No Secret Vault with the name has been registered + throw ($PodeLocale.noSecretVaultRegisteredExceptionMessage -f $Vault) } # get vault @@ -290,7 +291,8 @@ function Unlock-PodeSecretVault { if ($null -ne $expiry) { $expiry = ([datetime]$expiry).ToUniversalTime() if ($expiry -le [datetime]::UtcNow) { - throw "Secret Vault unlock expiry date is in the past (UTC): $($expiry)" + # Secret Vault unlock expiry date is in the past (UTC) + throw ($PodeLocale.secretVaultUnlockExpiryDateInPastExceptionMessage -f $expiry) } $vault.Unlock.Expiry = $expiry @@ -435,17 +437,20 @@ function Mount-PodeSecret { # has the secret been mounted already? if (Test-PodeSecret -Name $Name) { - throw "A Secret with the name '$($Name)' has already been mounted" + # A Secret with the name has already been mounted + throw ($PodeLocale.secretAlreadyMountedExceptionMessage -f $Name) } # does the vault exist? if (!(Test-PodeSecretVault -Name $Vault)) { - throw "No Secret Vault with the name '$($Vault)' has been registered" + # No Secret Vault with the name has been registered + throw ($PodeLocale.noSecretVaultRegisteredExceptionMessage -f $Vault) } # check properties if (!(Test-PodeIsEmpty $Property) -and !(Test-PodeIsEmpty $ExpandProperty)) { - throw 'You can only provide one of either Property or ExpandPropery, but not both' + # Parameters 'Property' and 'ExpandPropery' are mutually exclusive + throw ($PodeLocale.parametersMutuallyExclusiveExceptionMessage -f 'Property' , 'ExpandPropery') } # which cache value? @@ -508,7 +513,8 @@ function Dismount-PodeSecret { # do nothing if the secret hasn't been mounted, unless Remove is specified if (!(Test-PodeSecret -Name $Name)) { if ($Remove) { - throw "No Secret with the name '$($Name)' has been mounted to be removed from a Secret Vault" + # No Secret named has been mounted + throw ($PodeLocale.noSecretNamedMountedExceptionMessage -f $Name) } return @@ -550,7 +556,8 @@ function Get-PodeSecret { # has the secret been mounted? if (!(Test-PodeSecret -Name $Name)) { - throw "No Secret with the name '$($Name)' has been mounted" + # No Secret named has been mounted + throw ($PodeLocale.noSecretNamedMountedExceptionMessage -f $Name) } # get the secret and vault @@ -653,7 +660,7 @@ function Update-PodeSecret { $Name, #> byte[], string, securestring, pscredential, hashtable - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true )] [object] $InputObject, @@ -661,41 +668,56 @@ function Update-PodeSecret { [hashtable] $Metadata ) + begin { + # has the secret been mounted? + if (!(Test-PodeSecret -Name $Name)) { + # No Secret named has been mounted + throw ($PodeLocale.noSecretNamedMountedExceptionMessage -f $Name) + } - # has the secret been mounted? - if (!(Test-PodeSecret -Name $Name)) { - throw "No Secret with the name '$($Name)' has been mounted" + $pipelineItemCount = 0 # Initialize counter to track items in the pipeline. } - # make sure the value type is correct - $InputObject = Protect-PodeSecretValueType -Value $InputObject + process { + $pipelineItemCount++ # Increment the counter for each item in the pipeline. + } - # get the secret and vault - $secret = $PodeContext.Server.Secrets.Keys[$Name] + end { + # Throw an error if more than one item is passed in the pipeline. + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } - # reset the cache if enabled - if ($secret.Cache.Enabled) { - $secret.Cache.Value = $InputObject - $secret.Cache.Expiry = [datetime]::UtcNow.AddMinutes($secret.Cache.Ttl) - } + # make sure the value type is correct + $InputObject = Protect-PodeSecretValueType -Value $InputObject + + # get the secret and vault + $secret = $PodeContext.Server.Secrets.Keys[$Name] - # if we're expanding a property, convert this to a hashtable - if ($secret.Properties.Enabled -and $secret.Properties.Expand) { - $InputObject = @{ - "$($secret.Properties.Fields)" = $InputObject + # reset the cache if enabled + if ($secret.Cache.Enabled) { + $secret.Cache.Value = $InputObject + $secret.Cache.Expiry = [datetime]::UtcNow.AddMinutes($secret.Cache.Ttl) } - } - # set the secret depending on vault type - $vault = $PodeContext.Server.Secrets.Vaults[$secret.Vault] - Lock-PodeObject -Name $vault.LockableName -ScriptBlock { - switch ($vault.Type) { - 'custom' { - Set-PodeSecretCustomKey -Vault $secret.Vault -Key $secret.Key -Value $InputObject -Metadata $Metadata -ArgumentList $secret.Arguments + # if we're expanding a property, convert this to a hashtable + if ($secret.Properties.Enabled -and $secret.Properties.Expand) { + $InputObject = @{ + "$($secret.Properties.Fields)" = $InputObject } + } - 'secretmanagement' { - Set-PodeSecretManagementKey -Vault $secret.Vault -Key $secret.Key -Value $InputObject -Metadata $Metadata + # set the secret depending on vault type + $vault = $PodeContext.Server.Secrets.Vaults[$secret.Vault] + Lock-PodeObject -Name $vault.LockableName -ScriptBlock { + switch ($vault.Type) { + 'custom' { + Set-PodeSecretCustomKey -Vault $secret.Vault -Key $secret.Key -Value $InputObject -Metadata $Metadata -ArgumentList $secret.Arguments + } + + 'secretmanagement' { + Set-PodeSecretManagementKey -Vault $secret.Vault -Key $secret.Key -Value $InputObject -Metadata $Metadata + } } } } @@ -738,7 +760,8 @@ function Remove-PodeSecret { # has the vault been registered? if (!(Test-PodeSecretVault -Name $Vault)) { - throw "No Secret Vault with the name '$($Vault)' has been registered" + # No Secret Vault with the name has been registered + throw ($PodeLocale.noSecretVaultRegisteredExceptionMessage -f $Vault) } # remove the secret depending on vault type @@ -810,7 +833,8 @@ function Read-PodeSecret { # has the vault been registered? if (!(Test-PodeSecretVault -Name $Vault)) { - throw "No Secret Vault with the name '$($Vault)' has been registered" + # No Secret Vault with the name has been registered + throw ($PodeLocale.noSecretVaultRegisteredExceptionMessage -f $Vault) } # fetch the secret depending on vault type @@ -880,7 +904,7 @@ function Set-PodeSecret { $Vault, #> byte[], string, securestring, pscredential, hashtable - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [object] $InputObject, @@ -892,25 +916,40 @@ function Set-PodeSecret { [object[]] $ArgumentList ) + begin { + # has the vault been registered? + if (!(Test-PodeSecretVault -Name $Vault)) { + # No Secret Vault with the name has been registered + throw ($PodeLocale.noSecretVaultRegisteredExceptionMessage -f $Vault) + } - # has the vault been registered? - if (!(Test-PodeSecretVault -Name $Vault)) { - throw "No Secret Vault with the name '$($Vault)' has been registered" + $pipelineItemCount = 0 # Initialize counter to track items in the pipeline. } - # make sure the value type is correct - $InputObject = Protect-PodeSecretValueType -Value $InputObject + process { + $pipelineItemCount++ # Increment the counter for each item in the pipeline. + } - # set the secret depending on vault type - $_vault = $PodeContext.Server.Secrets.Vaults[$Vault] - Lock-PodeObject -Name $_vault.LockableName -ScriptBlock { - switch ($_vault.Type) { - 'custom' { - Set-PodeSecretCustomKey -Vault $Vault -Key $Key -Value $InputObject -Metadata $Metadata -ArgumentList $ArgumentList - } + end { + # Throw an error if more than one item is passed in the pipeline. + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } - 'secretmanagement' { - Set-PodeSecretManagementKey -Vault $Vault -Key $Key -Value $InputObject -Metadata $Metadata + # make sure the value type is correct + $InputObject = Protect-PodeSecretValueType -Value $InputObject + + # set the secret depending on vault type + $_vault = $PodeContext.Server.Secrets.Vaults[$Vault] + Lock-PodeObject -Name $_vault.LockableName -ScriptBlock { + switch ($_vault.Type) { + 'custom' { + Set-PodeSecretCustomKey -Vault $Vault -Key $Key -Value $InputObject -Metadata $Metadata -ArgumentList $ArgumentList + } + + 'secretmanagement' { + Set-PodeSecretManagementKey -Vault $Vault -Key $Key -Value $InputObject -Metadata $Metadata + } } } } diff --git a/src/Public/Security.ps1 b/src/Public/Security.ps1 index 3b360f8af..ee5ab7947 100644 --- a/src/Public/Security.ps1 +++ b/src/Public/Security.ps1 @@ -223,6 +223,7 @@ The Type to use. Set-PodeSecurityFrameOptions -Type SameOrigin #> function Set-PodeSecurityFrameOptions { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param( [Parameter(Mandatory = $true)] @@ -245,6 +246,7 @@ Removes definition for the X-Frame-Options header. Remove-PodeSecurityFrameOptions #> function Remove-PodeSecurityFrameOptions { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param() @@ -385,46 +387,7 @@ function Set-PodeSecurityContentSecurityPolicy { $XssBlock ) - # build the header's value - $values = @( - Protect-PodeContentSecurityKeyword -Name 'default-src' -Value $Default - Protect-PodeContentSecurityKeyword -Name 'child-src' -Value $Child - Protect-PodeContentSecurityKeyword -Name 'connect-src' -Value $Connect - Protect-PodeContentSecurityKeyword -Name 'font-src' -Value $Font - Protect-PodeContentSecurityKeyword -Name 'frame-src' -Value $Frame - Protect-PodeContentSecurityKeyword -Name 'img-src' -Value $Image - Protect-PodeContentSecurityKeyword -Name 'manifest-src' -Value $Manifest - Protect-PodeContentSecurityKeyword -Name 'media-src' -Value $Media - Protect-PodeContentSecurityKeyword -Name 'object-src' -Value $Object - Protect-PodeContentSecurityKeyword -Name 'script-src' -Value $Scripts - Protect-PodeContentSecurityKeyword -Name 'style-src' -Value $Style - Protect-PodeContentSecurityKeyword -Name 'base-uri' -Value $BaseUri - Protect-PodeContentSecurityKeyword -Name 'form-action' -Value $FormAction - Protect-PodeContentSecurityKeyword -Name 'frame-ancestors' -Value $FrameAncestor - ) - - if ($Sandbox -ine 'None') { - $values += "sandbox $($Sandbox.ToLowerInvariant())".Trim() - } - - if ($UpgradeInsecureRequests) { - $values += 'upgrade-insecure-requests' - } - - $values = ($values -ne $null) - $value = ($values -join '; ') - - # add the header - Add-PodeSecurityHeader -Name 'Content-Security-Policy' -Value $value - - # this is done to explicitly disable XSS auditors in modern browsers - # as having it enabled has now been found to cause more vulnerabilities - if ($XssBlock) { - Add-PodeSecurityHeader -Name 'X-XSS-Protection' -Value '1; mode=block' - } - else { - Add-PodeSecurityHeader -Name 'X-XSS-Protection' -Value '0' - } + Set-PodeSecurityContentSecurityPolicyInternal -Params $PSBoundParameters } <# @@ -486,6 +449,7 @@ If supplied, the header will have the upgrade-insecure-requests value added. Add-PodeSecurityContentSecurityPolicy -Default '*.twitter.com' -Image 'data' #> function Add-PodeSecurityContentSecurityPolicy { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSPossibleIncorrectComparisonWithNull', '')] [CmdletBinding()] param( [Parameter()] @@ -555,37 +519,7 @@ function Add-PodeSecurityContentSecurityPolicy { $UpgradeInsecureRequests ) - # build the header's value - $values = @( - Protect-PodeContentSecurityKeyword -Name 'default-src' -Value $Default -Append - Protect-PodeContentSecurityKeyword -Name 'child-src' -Value $Child -Append - Protect-PodeContentSecurityKeyword -Name 'connect-src' -Value $Connect -Append - Protect-PodeContentSecurityKeyword -Name 'font-src' -Value $Font -Append - Protect-PodeContentSecurityKeyword -Name 'frame-src' -Value $Frame -Append - Protect-PodeContentSecurityKeyword -Name 'img-src' -Value $Image -Append - Protect-PodeContentSecurityKeyword -Name 'manifest-src' -Value $Manifest -Append - Protect-PodeContentSecurityKeyword -Name 'media-src' -Value $Media -Append - Protect-PodeContentSecurityKeyword -Name 'object-src' -Value $Object -Append - Protect-PodeContentSecurityKeyword -Name 'script-src' -Value $Scripts -Append - Protect-PodeContentSecurityKeyword -Name 'style-src' -Value $Style -Append - Protect-PodeContentSecurityKeyword -Name 'base-uri' -Value $BaseUri -Append - Protect-PodeContentSecurityKeyword -Name 'form-action' -Value $FormAction -Append - Protect-PodeContentSecurityKeyword -Name 'frame-ancestors' -Value $FrameAncestor -Append - ) - - if ($Sandbox -ine 'None') { - $values += "sandbox $($Sandbox.ToLowerInvariant())".Trim() - } - - if ($UpgradeInsecureRequests) { - $values += 'upgrade-insecure-requests' - } - - $values = ($values -ne $null) - $value = ($values -join '; ') - - # add the header - Add-PodeSecurityHeader -Name 'Content-Security-Policy' -Value $value + Set-PodeSecurityContentSecurityPolicyInternal -Params $PSBoundParameters -Append } <# @@ -703,10 +637,9 @@ The values to use for the WebShare portion of the header. .PARAMETER XrSpatialTracking The values to use for the XrSpatialTracking portion of the header. -.EXAMPLE -Set-PodeSecurityPermissionsPolicy -LayoutAnimations 'none' -UnoptimisedImages 'none' -OversizedImages 'none' -SyncXhr 'none' -UnsizedMedia 'none' #> function Set-PodeSecurityPermissionsPolicy { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSPossibleIncorrectComparisonWithNull', '')] [CmdletBinding()] param( [Parameter()] @@ -830,45 +763,7 @@ function Set-PodeSecurityPermissionsPolicy { $XrSpatialTracking ) - # build the header's value - $values = @( - Protect-PodePermissionsPolicyKeyword -Name 'accelerometer' -Value $Accelerometer - Protect-PodePermissionsPolicyKeyword -Name 'ambient-light-sensor' -Value $AmbientLightSensor - Protect-PodePermissionsPolicyKeyword -Name 'autoplay' -Value $Autoplay - Protect-PodePermissionsPolicyKeyword -Name 'battery' -Value $Battery - Protect-PodePermissionsPolicyKeyword -Name 'camera' -Value $Camera - Protect-PodePermissionsPolicyKeyword -Name 'display-capture' -Value $DisplayCapture - Protect-PodePermissionsPolicyKeyword -Name 'document-domain' -Value $DocumentDomain - Protect-PodePermissionsPolicyKeyword -Name 'encrypted-media' -Value $EncryptedMedia - Protect-PodePermissionsPolicyKeyword -Name 'fullscreen' -Value $Fullscreen - Protect-PodePermissionsPolicyKeyword -Name 'gamepad' -Value $Gamepad - Protect-PodePermissionsPolicyKeyword -Name 'geolocation' -Value $Geolocation - Protect-PodePermissionsPolicyKeyword -Name 'gyroscope' -Value $Gyroscope - Protect-PodePermissionsPolicyKeyword -Name 'interest-cohort' -Value $InterestCohort - Protect-PodePermissionsPolicyKeyword -Name 'layout-animations' -Value $LayoutAnimations - Protect-PodePermissionsPolicyKeyword -Name 'legacy-image-formats' -Value $LegacyImageFormats - Protect-PodePermissionsPolicyKeyword -Name 'magnetometer' -Value $Magnetometer - Protect-PodePermissionsPolicyKeyword -Name 'microphone' -Value $Microphone - Protect-PodePermissionsPolicyKeyword -Name 'midi' -Value $Midi - Protect-PodePermissionsPolicyKeyword -Name 'oversized-images' -Value $OversizedImages - Protect-PodePermissionsPolicyKeyword -Name 'payment' -Value $Payment - Protect-PodePermissionsPolicyKeyword -Name 'picture-in-picture' -Value $PictureInPicture - Protect-PodePermissionsPolicyKeyword -Name 'publickey-credentials-get' -Value $PublicKeyCredentials - Protect-PodePermissionsPolicyKeyword -Name 'speaker-selection' -Value $Speakers - Protect-PodePermissionsPolicyKeyword -Name 'sync-xhr' -Value $SyncXhr - Protect-PodePermissionsPolicyKeyword -Name 'unoptimized-images' -Value $UnoptimisedImages - Protect-PodePermissionsPolicyKeyword -Name 'unsized-media' -Value $UnsizedMedia - Protect-PodePermissionsPolicyKeyword -Name 'usb' -Value $Usb - Protect-PodePermissionsPolicyKeyword -Name 'screen-wake-lock' -Value $ScreenWakeLake - Protect-PodePermissionsPolicyKeyword -Name 'web-share' -Value $WebShare - Protect-PodePermissionsPolicyKeyword -Name 'xr-spatial-tracking' -Value $XrSpatialTracking - ) - - $values = ($values -ne $null) - $value = ($values -join ', ') - - # add the header - Add-PodeSecurityHeader -Name 'Permissions-Policy' -Value $value + Set-PodeSecurityPermissionsPolicyInternal -Params $PSBoundParameters } <# @@ -1095,45 +990,7 @@ function Add-PodeSecurityPermissionsPolicy { $XrSpatialTracking ) - # build the header's value - $values = @( - Protect-PodePermissionsPolicyKeyword -Name 'accelerometer' -Value $Accelerometer -Append - Protect-PodePermissionsPolicyKeyword -Name 'ambient-light-sensor' -Value $AmbientLightSensor -Append - Protect-PodePermissionsPolicyKeyword -Name 'autoplay' -Value $Autoplay -Append - Protect-PodePermissionsPolicyKeyword -Name 'battery' -Value $Battery -Append - Protect-PodePermissionsPolicyKeyword -Name 'camera' -Value $Camera -Append - Protect-PodePermissionsPolicyKeyword -Name 'display-capture' -Value $DisplayCapture -Append - Protect-PodePermissionsPolicyKeyword -Name 'document-domain' -Value $DocumentDomain -Append - Protect-PodePermissionsPolicyKeyword -Name 'encrypted-media' -Value $EncryptedMedia -Append - Protect-PodePermissionsPolicyKeyword -Name 'fullscreen' -Value $Fullscreen -Append - Protect-PodePermissionsPolicyKeyword -Name 'gamepad' -Value $Gamepad -Append - Protect-PodePermissionsPolicyKeyword -Name 'geolocation' -Value $Geolocation -Append - Protect-PodePermissionsPolicyKeyword -Name 'gyroscope' -Value $Gyroscope -Append - Protect-PodePermissionsPolicyKeyword -Name 'interest-cohort' -Value $InterestCohort -Append - Protect-PodePermissionsPolicyKeyword -Name 'layout-animations' -Value $LayoutAnimations -Append - Protect-PodePermissionsPolicyKeyword -Name 'legacy-image-formats' -Value $LegacyImageFormats -Append - Protect-PodePermissionsPolicyKeyword -Name 'magnetometer' -Value $Magnetometer -Append - Protect-PodePermissionsPolicyKeyword -Name 'microphone' -Value $Microphone -Append - Protect-PodePermissionsPolicyKeyword -Name 'midi' -Value $Midi -Append - Protect-PodePermissionsPolicyKeyword -Name 'oversized-images' -Value $OversizedImages -Append - Protect-PodePermissionsPolicyKeyword -Name 'payment' -Value $Payment -Append - Protect-PodePermissionsPolicyKeyword -Name 'picture-in-picture' -Value $PictureInPicture -Append - Protect-PodePermissionsPolicyKeyword -Name 'publickey-credentials-get' -Value $PublicKeyCredentials -Append - Protect-PodePermissionsPolicyKeyword -Name 'speaker-selection' -Value $Speakers -Append - Protect-PodePermissionsPolicyKeyword -Name 'sync-xhr' -Value $SyncXhr -Append - Protect-PodePermissionsPolicyKeyword -Name 'unoptimized-images' -Value $UnoptimisedImages -Append - Protect-PodePermissionsPolicyKeyword -Name 'unsized-media' -Value $UnsizedMedia -Append - Protect-PodePermissionsPolicyKeyword -Name 'usb' -Value $Usb -Append - Protect-PodePermissionsPolicyKeyword -Name 'screen-wake-lock' -Value $ScreenWakeLake -Append - Protect-PodePermissionsPolicyKeyword -Name 'web-share' -Value $WebShare -Append - Protect-PodePermissionsPolicyKeyword -Name 'xr-spatial-tracking' -Value $XrSpatialTracking -Append - ) - - $values = ($values -ne $null) - $value = ($values -join ', ') - - # add the header - Add-PodeSecurityHeader -Name 'Permissions-Policy' -Value $value + Set-PodeSecurityPermissionsPolicyInternal -Params $PSBoundParameters -Append } <# @@ -1207,6 +1064,7 @@ Set a value for the X-Content-Type-Options header to "nosniff". Set-PodeSecurityContentTypeOptions #> function Set-PodeSecurityContentTypeOptions { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param() @@ -1224,6 +1082,7 @@ Removes definitions for the X-Content-Type-Options header. Remove-PodeSecurityContentTypeOptions #> function Remove-PodeSecurityContentTypeOptions { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param() @@ -1258,7 +1117,8 @@ function Set-PodeSecurityStrictTransportSecurity { ) if ($Duration -le 0) { - throw "Invalid Strict-Transport-Security duration supplied: $($Duration). Should be greater than 0" + # Invalid Strict-Transport-Security duration supplied + throw ($PodeLocale.invalidStrictTransportSecurityDurationExceptionMessage -f $Duration) } $value = "max-age=$($Duration)" @@ -1453,7 +1313,8 @@ function Set-PodeSecurityAccessControl { if (![string]::IsNullOrWhiteSpace($Headers) -or $AuthorizationHeader -or $CrossDomainXhrRequests) { if ($Headers -icontains '*') { if ($Credentials) { - throw 'The * wildcard for Headers, when Credentials is passed, will be taken as a literal string and not a wildcard' + # When Credentials is passed, The * wildcard for Headers will be taken as a literal string and not a wildcard + throw ($PodeLocale.credentialsPassedWildcardForHeadersLiteralExceptionMessage) } $Headers = @('*') @@ -1478,7 +1339,8 @@ function Set-PodeSecurityAccessControl { if ($AutoHeaders) { if ($Headers -icontains '*') { - throw 'The * wildcard for Headers, is not comptatibile with the AutoHeaders switch' + # The * wildcard for Headers is incompatible with the AutoHeaders switch + throw ($PodeLocale.wildcardHeadersIncompatibleWithAutoHeadersExceptionMessage) } Add-PodeSecurityHeader -Name 'Access-Control-Allow-Headers' -Value 'content-type' -Append @@ -1487,7 +1349,8 @@ function Set-PodeSecurityAccessControl { if ($AutoMethods) { if ($Methods -icontains '*') { - throw 'The * wildcard for Methods, is not comptatibile with the AutoMethods switch' + # The * wildcard for Methods is incompatible with the AutoMethods switch + throw ($PodeLocale.wildcardMethodsIncompatibleWithAutoMethodsExceptionMessage) } if ($WithOptions) { Add-PodeSecurityHeader -Name 'Access-Control-Allow-Methods' -Value 'Options' -Append @@ -1497,7 +1360,8 @@ function Set-PodeSecurityAccessControl { # duration if ($Duration -le 0) { - throw "Invalid Access-Control-Max-Age duration supplied: $($Duration). Should be greater than 0" + # Invalid Access-Control-Max-Age duration supplied + throw ($PodeLocale.invalidAccessControlMaxAgeDurationExceptionMessage -f $Duration) } Add-PodeSecurityHeader -Name 'Access-Control-Max-Age' -Value $Duration diff --git a/src/Public/Sessions.ps1 b/src/Public/Sessions.ps1 index 55b3991e3..5ffbbb80c 100644 --- a/src/Public/Sessions.ps1 +++ b/src/Public/Sessions.ps1 @@ -66,7 +66,8 @@ function Enable-PodeSessionMiddleware { [Parameter()] [ValidateScript({ if ($_ -lt 0) { - throw "Duration must be 0 or greater, but got: $($_)s" + # Duration must be 0 or greater, but got + throw ($PodeLocale.durationMustBeZeroOrGreaterExceptionMessage -f $_) } return $true @@ -108,7 +109,8 @@ function Enable-PodeSessionMiddleware { # check that session logic hasn't already been initialised if (Test-PodeSessionsEnabled) { - throw 'Session Middleware has already been intialised' + # Session Middleware has already been initialized + throw ($PodeLocale.sessionMiddlewareAlreadyInitializedExceptionMessage) } # ensure the override store has the required methods @@ -116,7 +118,8 @@ function Enable-PodeSessionMiddleware { $members = @($Storage | Get-Member | Select-Object -ExpandProperty Name) @('delete', 'get', 'set') | ForEach-Object { if ($members -inotcontains $_) { - throw "Custom session storage does not implement the required '$($_)()' method" + # The custom session storage does not implement the required '{0}()' method + throw ($PodeLocale.customSessionStorageMethodNotImplementedExceptionMessage -f $_) } } } @@ -124,7 +127,8 @@ function Enable-PodeSessionMiddleware { # verify the secret, set to guid if not supplied, or error if none and we have a storage if ([string]::IsNullOrEmpty($Secret)) { if (!(Test-PodeIsEmpty $Storage)) { - throw 'A Secret is required when using custom session storage' + # A Secret is required when using custom session storage + throw ($PodeLocale.secretRequiredForCustomSessionStorageExceptionMessage) } $Secret = Get-PodeServerDefaultSecret @@ -178,7 +182,8 @@ function Remove-PodeSession { # if sessions haven't been setup, error if (!(Test-PodeSessionsEnabled)) { - throw 'Sessions have not been configured' + # The sessions have not been configured + throw ($PodeLocale.sessionsNotConfiguredExceptionMessage) } # do nothing if session is null @@ -212,12 +217,14 @@ function Save-PodeSession { # if sessions haven't been setup, error if (!(Test-PodeSessionsEnabled)) { - throw 'Sessions have not been configured' + # The sessions have not been configured + throw ($PodeLocale.sessionsNotConfiguredExceptionMessage) } # error if session is null if ($null -eq $WebEvent.Session) { - throw 'There is no session available to save' + # There is no session available to save + throw ($PodeLocale.noSessionAvailableToSaveExceptionMessage) } # if auth is in use, then assign to session store @@ -306,12 +313,14 @@ function Reset-PodeSessionExpiry { # if sessions haven't been setup, error if (!(Test-PodeSessionsEnabled)) { - throw 'Sessions have not been configured' + # The sessions have not been configured + throw ($PodeLocale.sessionsNotConfiguredExceptionMessage) } # error if session is null if ($null -eq $WebEvent.Session) { - throw 'There is no session available to save' + # There is no session available to save + throw ($PodeLocale.noSessionAvailableToSaveExceptionMessage) } # temporarily set this session to auto-extend @@ -333,6 +342,7 @@ $duration = Get-PodeSessionDuration #> function Get-PodeSessionDuration { [CmdletBinding()] + [OutputType([int])] param() return [int]$PodeContext.Server.Sessions.Info.Duration @@ -350,11 +360,13 @@ $expiry = Get-PodeSessionExpiry #> function Get-PodeSessionExpiry { [CmdletBinding()] + [OutputType([datetime])] param() # error if session is null if ($null -eq $WebEvent.Session) { - throw 'There is no session available to save' + # There is no session available to save + throw ($PodeLocale.noSessionAvailableToSaveExceptionMessage) } # default min date diff --git a/src/Public/State.ps1 b/src/Public/State.ps1 index 34d8519bb..1c841e586 100644 --- a/src/Public/State.ps1 +++ b/src/Public/State.ps1 @@ -28,7 +28,7 @@ function Set-PodeState { [string] $Name, - [Parameter(ValueFromPipeline = $true)] + [Parameter(ValueFromPipeline = $true, Position = 0)] [object] $Value, @@ -37,20 +37,38 @@ function Set-PodeState { $Scope ) - if ($null -eq $PodeContext.Server.State) { - throw 'Pode has not been initialised' - } + begin { + if ($null -eq $PodeContext.Server.State) { + # Pode has not been initialized + throw ($PodeLocale.podeNotInitializedExceptionMessage) + } - if ($null -eq $Scope) { - $Scope = @() + if ($null -eq $Scope) { + $Scope = @() + } + + # Initialize an array to hold piped-in values + $pipelineValue = @() } - $PodeContext.Server.State[$Name] = @{ - Value = $Value - Scope = $Scope + process { + # Add the current piped-in value to the array + $pipelineValue += $_ } - return $Value + end { + # Set Value to the array of values + if ($pipelineValue.Count -gt 1) { + $Value = $pipelineValue + } + + $PodeContext.Server.State[$Name] = @{ + Value = $Value + Scope = $Scope + } + + return $Value + } } <# @@ -81,7 +99,8 @@ function Get-PodeState { ) if ($null -eq $PodeContext.Server.State) { - throw 'Pode has not been initialised' + # Pode has not been initialized + throw ($PodeLocale.podeNotInitializedExceptionMessage) } if ($WithScope) { @@ -112,6 +131,7 @@ $names = Get-PodeStateNames -Scope '' $names = Get-PodeStateNames -Pattern '^\w+[0-9]{0,2}$' #> function Get-PodeStateNames { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param( [Parameter()] @@ -124,7 +144,8 @@ function Get-PodeStateNames { ) if ($null -eq $PodeContext.Server.State) { - throw 'Pode has not been initialised' + # Pode has not been initialized + throw ($PodeLocale.podeNotInitializedExceptionMessage) } if ($null -eq $Scope) { @@ -176,7 +197,8 @@ function Remove-PodeState { ) if ($null -eq $PodeContext.Server.State) { - throw 'Pode has not been initialised' + # Pode has not been initialized + throw ($PodeLocale.podeNotInitializedExceptionMessage) } $value = $PodeContext.Server.State[$Name].Value @@ -247,7 +269,8 @@ function Save-PodeState { # error if attempting to use outside of the pode server if ($null -eq $PodeContext.Server.State) { - throw 'Pode has not been initialised' + # Pode has not been initialized + throw ($PodeLocale.podeNotInitializedExceptionMessage) } # get the full path to save the state @@ -341,7 +364,8 @@ function Restore-PodeState { # error if attempting to use outside of the pode server if ($null -eq $PodeContext.Server.State) { - throw 'Pode has not been initialised' + # Pode has not been initialized + throw ($PodeLocale.podeNotInitializedExceptionMessage) } # get the full path to the state @@ -415,8 +439,9 @@ function Test-PodeState { ) if ($null -eq $PodeContext.Server.State) { - throw 'Pode has not been initialised' + # Pode has not been initialized + throw ($PodeLocale.podeNotInitializedExceptionMessage) } return $PodeContext.Server.State.ContainsKey($Name) -} +} \ No newline at end of file diff --git a/src/Public/Tasks.ps1 b/src/Public/Tasks.ps1 index d4f47573c..26477d7c6 100644 --- a/src/Public/Tasks.ps1 +++ b/src/Public/Tasks.ps1 @@ -1,27 +1,33 @@ <# .SYNOPSIS -Adds a new Task. + Adds a new Task. .DESCRIPTION -Adds a new Task, which can be asynchronously or synchronously invoked. + Adds a new Task, which can be asynchronously or synchronously invoked. .PARAMETER Name -The Name of the Task. + The Name of the Task. .PARAMETER ScriptBlock -The script for the Task. + The script for the Task. .PARAMETER FilePath -A literal, or relative, path to a file containing a ScriptBlock for the Task's logic. + A literal, or relative, path to a file containing a ScriptBlock for the Task's logic. .PARAMETER ArgumentList -A hashtable of arguments to supply to the Task's ScriptBlock. + A hashtable of arguments to supply to the Task's ScriptBlock. + +.PARAMETER Timeout + A Timeout, in seconds, to abort running the Task process. (Default: -1 [never timeout]) + +.PARAMETER TimeoutFrom + Where to start the Timeout from, either 'Create', 'Start'. (Default: 'Create') .EXAMPLE -Add-PodeTask -Name 'Example1' -ScriptBlock { Invoke-SomeLogic } + Add-PodeTask -Name 'Example1' -ScriptBlock { Invoke-SomeLogic } .EXAMPLE -Add-PodeTask -Name 'Example1' -ScriptBlock { return Get-SomeObject } + Add-PodeTask -Name 'Example1' -ScriptBlock { return Get-SomeObject } #> function Add-PodeTask { [CmdletBinding(DefaultParameterSetName = 'Script')] @@ -40,11 +46,21 @@ function Add-PodeTask { [Parameter()] [hashtable] - $ArgumentList + $ArgumentList, + + [Parameter()] + [int] + $Timeout = -1, + + [Parameter()] + [ValidateSet('Create', 'Start')] + [string] + $TimeoutFrom = 'Create' ) # ensure the task doesn't already exist if ($PodeContext.Tasks.Items.ContainsKey($Name)) { - throw "[Task] $($Name): Task already defined" + # [Task] Task already defined + throw ($PodeLocale.taskAlreadyDefinedExceptionMessage -f $Name) } # if we have a file path supplied, load that path as a scriptblock @@ -62,6 +78,10 @@ function Add-PodeTask { Script = $ScriptBlock UsingVariables = $usingVars Arguments = (Protect-PodeValue -Value $ArgumentList -Default @{}) + Timeout = @{ + Value = $Timeout + From = $TimeoutFrom + } } } @@ -88,7 +108,9 @@ function Set-PodeTaskConcurrency { # error if <=0 if ($Maximum -le 0) { - throw "Maximum concurrent tasks must be >=1 but got: $($Maximum)" + # Maximum concurrent tasks must be >=1 but got + throw ($PodeLocale.maximumConcurrentTasksInvalidExceptionMessage -f $Maximum) + } # ensure max > min @@ -98,7 +120,8 @@ function Set-PodeTaskConcurrency { } if ($_min -gt $Maximum) { - throw "Maximum concurrent tasks cannot be less than the minimum of $($_min) but got: $($Maximum)" + # Maximum concurrent tasks cannot be less than the minimum of $_min but got $Maximum + throw ($PodeLocale.maximumConcurrentTasksLessThanMinimumExceptionMessage -f $_min, $Maximum) } # set the max tasks @@ -114,6 +137,7 @@ Invoke a Task. .DESCRIPTION Invoke a Task either asynchronously or synchronously, with support for returning values. +The function returns the Task process onbject which was triggered. .PARAMETER Name The Name of the Task. @@ -122,10 +146,16 @@ The Name of the Task. A hashtable of arguments to supply to the Task's ScriptBlock. .PARAMETER Timeout -A Timeout, in seconds, to abort running the task. (Default: -1 [never timeout]) +A Timeout, in seconds, to abort running the Task process. (Default: -1 [never timeout]) + +.PARAMETER TimeoutFrom +Where to start the Timeout from, either 'Default', 'Create', or 'Start'. (Default: 'Default' - will use the value from Add-PodeTask) .PARAMETER Wait -If supplied, Pode will wait until the Task has finished executing, and then return any values. +If supplied, Pode will wait until the Task process has finished executing, and then return any values. + +.OUTPUTS +The triggered Task process. .EXAMPLE Invoke-PodeTask -Name 'Example1' -Wait -Timeout 5 @@ -139,7 +169,7 @@ Invoke-PodeTask -Name 'Example1' | Wait-PodeTask -Timeout 3 function Invoke-PodeTask { [CmdletBinding()] param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [string] $Name, @@ -151,25 +181,32 @@ function Invoke-PodeTask { [int] $Timeout = -1, + [Parameter()] + [ValidateSet('Default', 'Create', 'Start')] + [string] + $TimeoutFrom = 'Default', + [switch] $Wait ) - - # ensure the task exists - if (!$PodeContext.Tasks.Items.ContainsKey($Name)) { - throw "Task '$($Name)' does not exist" + process { + # ensure the task exists + if (!$PodeContext.Tasks.Items.ContainsKey($Name)) { + # Task does not exist + throw ($PodeLocale.taskDoesNotExistExceptionMessage -f $Name) + } + + # run task logic + $task = Invoke-PodeInternalTask -Task $PodeContext.Tasks.Items[$Name] -ArgumentList $ArgumentList -Timeout $Timeout -TimeoutFrom $TimeoutFrom + + # wait, and return result? + if ($Wait) { + return (Wait-PodeTask -Task $task -Timeout $Timeout) + } + + # return task + return $task } - - # run task logic - $task = Invoke-PodeInternalTask -Task $PodeContext.Tasks.Items[$Name] -ArgumentList $ArgumentList -Timeout $Timeout - - # wait, and return result? - if ($Wait) { - return (Wait-PodeTask -Task $task -Timeout $Timeout) - } - - # return task - return $task } <# @@ -188,12 +225,13 @@ Remove-PodeTask -Name 'Example1' function Remove-PodeTask { [CmdletBinding()] param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [string] $Name ) - - $null = $PodeContext.Tasks.Items.Remove($Name) + process { + $null = $PodeContext.Tasks.Items.Remove($Name) + } } <# @@ -207,6 +245,7 @@ Removes all Tasks. Clear-PodeTasks #> function Clear-PodeTasks { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param() @@ -235,7 +274,7 @@ Edit-PodeTask -Name 'Example1' -ScriptBlock { Invoke-SomeNewLogic } function Edit-PodeTask { [CmdletBinding()] param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [string] $Name, @@ -247,24 +286,26 @@ function Edit-PodeTask { [hashtable] $ArgumentList ) - - # ensure the task exists - if (!$PodeContext.Tasks.Items.ContainsKey($Name)) { - throw "Task '$($Name)' does not exist" - } - - $_task = $PodeContext.Tasks.Items[$Name] - - # edit scriptblock if supplied - if (!(Test-PodeIsEmpty $ScriptBlock)) { - $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState - $_task.Script = $ScriptBlock - $_task.UsingVariables = $usingVars - } - - # edit arguments if supplied - if (!(Test-PodeIsEmpty $ArgumentList)) { - $_task.Arguments = $ArgumentList + process { + # ensure the task exists + if (!$PodeContext.Tasks.Items.ContainsKey($Name)) { + # Task does not exist + throw ($PodeLocale.taskDoesNotExistExceptionMessage -f $Name) + } + + $_task = $PodeContext.Tasks.Items[$Name] + + # edit scriptblock if supplied + if (!(Test-PodeIsEmpty $ScriptBlock)) { + $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + $_task.Script = $ScriptBlock + $_task.UsingVariables = $usingVars + } + + # edit arguments if supplied + if (!(Test-PodeIsEmpty $ArgumentList)) { + $_task.Arguments = $ArgumentList + } } } @@ -328,6 +369,7 @@ Use-PodeTasks Use-PodeTasks -Path './my-tasks' #> function Use-PodeTasks { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param( [Parameter()] @@ -354,47 +396,50 @@ Invoke-PodeTask -Name 'Example1' | Close-PodeTask function Close-PodeTask { [CmdletBinding()] param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [hashtable] $Task ) - - Close-PodeTaskInternal -Result $Task + process { + Close-PodeTaskInternal -Process $Task + } } <# .SYNOPSIS -Test if a running Task has completed. +Test if a running Task process has completed. .DESCRIPTION -Test if a running Task has completed. +Test if a running Task process has completed. .PARAMETER Task -The Task to be check. +The Task process to be check. The process returned by either Invoke-PodeTask or Get-PodeTaskProcess. .EXAMPLE Invoke-PodeTask -Name 'Example1' | Test-PodeTaskCompleted #> function Test-PodeTaskCompleted { [CmdletBinding()] + [OutputType([bool])] param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [hashtable] $Task ) - - return [bool]$Task.Runspace.Handler.IsCompleted + process { + return [bool]$Task.Runspace.Handler.IsCompleted + } } <# .SYNOPSIS -Waits for a task to finish, and returns a result if there is one. +Waits for a Task process to finish, and returns a result if there is one. .DESCRIPTION -Waits for a task to finish, and returns a result if there is one. +Waits for a Task process to finish, and returns a result if there is one. .PARAMETER Task -The task to wait on. +The Task process to wait on. The process returned by either Invoke-PodeTask or Get-PodeTaskProcess. .PARAMETER Timeout An optional Timeout in milliseconds. @@ -409,21 +454,211 @@ function Wait-PodeTask { [CmdletBinding()] [OutputType([object])] param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] $Task, [Parameter()] [int] $Timeout = -1 ) + begin { + $pipelineItemCount = 0 + } - if ($Task -is [System.Threading.Tasks.Task]) { - return (Wait-PodeNetTaskInternal -Task $Task -Timeout $Timeout) + process { + $pipelineItemCount++ } - if ($Task -is [hashtable]) { - return (Wait-PodeTaskInternal -Task $Task -Timeout $Timeout) + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + if ($Task -is [System.Threading.Tasks.Task]) { + return (Wait-PodeNetTaskInternal -Task $Task -Timeout $Timeout) + } + + if ($Task -is [hashtable]) { + return (Wait-PodeTaskInternal -Task $Task -Timeout $Timeout) + } + + # Task type is invalid, expected either [System.Threading.Tasks.Task] or [hashtable] + throw ($PodeLocale.invalidTaskTypeExceptionMessage) + } +} + +<# +.SYNOPSIS +Get all Task Processes. + +.DESCRIPTION +Get all Task Processes, with support for filtering. These are the processes created when using Invoke-PodeTask. + +.PARAMETER Name +An optional Name of the Task to filter by, can be one or more. + +.PARAMETER Id +An optional ID of the Task process to filter by, can be one or more. + +.PARAMETER State +An optional State of the Task process to filter by, can be one or more. + +.EXAMPLE +Get-PodeTaskProcess + +.EXAMPLE +Get-PodeTaskProcess -Name 'TaskName' + +.EXAMPLE +Get-PodeTaskProcess -Id 'TaskId' + +.EXAMPLE +Get-PodeTaskProcess -State 'Running' +#> +function Get-PodeTaskProcess { + [CmdletBinding()] + param( + [Parameter()] + [string[]] + $Name, + + [Parameter()] + [string[]] + $Id, + + [Parameter()] + [ValidateSet('All', 'Pending', 'Running', 'Completed', 'Failed')] + [string[]] + $State = 'All' + ) + + $processes = $PodeContext.Tasks.Processes.Values + + # filter processes by name + if (($null -ne $Name) -and ($Name.Length -gt 0)) { + $processes = @(foreach ($_name in $Name) { + foreach ($process in $processes) { + if ($process.Task -ine $_name) { + continue + } + + $process + } + }) + } + + # filter processes by id + if (($null -ne $Id) -and ($Id.Length -gt 0)) { + $processes = @(foreach ($_id in $Id) { + foreach ($process in $processes) { + if ($process.ID -ine $_id) { + continue + } + + $process + } + }) + } + + # filter processes by status + if ($State -inotcontains 'All') { + $processes = @(foreach ($process in $processes) { + if ($State -inotcontains $process.State) { + continue + } + + $process + }) + } + + # return processes + return $processes +} + + +<# +.SYNOPSIS +Get all Task Processes. + +.DESCRIPTION +Get all Task Processes, with support for filtering. These are the processes created when using Invoke-PodeTask. + +.PARAMETER Name +An optional Name of the Task to filter by, can be one or more. + +.PARAMETER Id +An optional ID of the Task process to filter by, can be one or more. + +.PARAMETER State +An optional State of the Task process to filter by, can be one or more. + +.EXAMPLE +Get-PodeTaskProcess + +.EXAMPLE +Get-PodeTaskProcess -Name 'TaskName' + +.EXAMPLE +Get-PodeTaskProcess -Id 'TaskId' + +.EXAMPLE +Get-PodeTaskProcess -State 'Running' +#> +function Get-PodeTaskProcess { + [CmdletBinding()] + param( + [Parameter()] + [string[]] + $Name, + + [Parameter()] + [string[]] + $Id, + + [Parameter()] + [ValidateSet('All', 'Pending', 'Running', 'Completed', 'Failed')] + [string[]] + $State = 'All' + ) + + $processes = $PodeContext.Tasks.Processes.Values + + # filter processes by name + if (($null -ne $Name) -and ($Name.Length -gt 0)) { + $processes = @(foreach ($_name in $Name) { + foreach ($process in $processes) { + if ($process.Task -ine $_name) { + continue + } + + $process + } + }) + } + + # filter processes by id + if (($null -ne $Id) -and ($Id.Length -gt 0)) { + $processes = @(foreach ($_id in $Id) { + foreach ($process in $processes) { + if ($process.ID -ine $_id) { + continue + } + + $process + } + }) + } + + # filter processes by status + if ($State -inotcontains 'All') { + $processes = @(foreach ($process in $processes) { + if ($State -inotcontains $process.State) { + continue + } + + $process + }) } - throw 'Task type is invalid, expected either [System.Threading.Tasks.Task] or [hashtable]' + # return processes + return $processes } \ No newline at end of file diff --git a/src/Public/Threading.ps1 b/src/Public/Threading.ps1 index a8249d15e..c192c063f 100644 --- a/src/Public/Threading.ps1 +++ b/src/Public/Threading.ps1 @@ -39,7 +39,7 @@ function Lock-PodeObject { [CmdletBinding(DefaultParameterSetName = 'Object')] [OutputType([object])] param( - [Parameter(ValueFromPipeline = $true, ParameterSetName = 'Object')] + [Parameter(ValueFromPipeline = $true, Position = 0, ParameterSetName = 'Object')] [object] $Object, @@ -61,29 +61,41 @@ function Lock-PodeObject { [switch] $CheckGlobal ) + begin { + $pipelineItemCount = 0 + } - try { - if ([string]::IsNullOrEmpty($Name)) { - Enter-PodeLockable -Object $Object -Timeout $Timeout -CheckGlobal:$CheckGlobal - } - else { - Enter-PodeLockable -Name $Name -Timeout $Timeout -CheckGlobal:$CheckGlobal - } + process { + $pipelineItemCount++ + } - if ($null -ne $ScriptBlock) { - Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -NoNewClosure -Return:$Return + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) } - } - catch { - $_ | Write-PodeErrorLog - throw $_.Exception - } - finally { - if ([string]::IsNullOrEmpty($Name)) { - Exit-PodeLockable -Object $Object + try { + if ([string]::IsNullOrEmpty($Name)) { + Enter-PodeLockable -Object $Object -Timeout $Timeout -CheckGlobal:$CheckGlobal + } + else { + Enter-PodeLockable -Name $Name -Timeout $Timeout -CheckGlobal:$CheckGlobal + } + + if ($null -ne $ScriptBlock) { + Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -NoNewClosure -Return:$Return + } + } + catch { + $_ | Write-PodeErrorLog + throw $_.Exception } - else { - Exit-PodeLockable -Name $Name + finally { + if ([string]::IsNullOrEmpty($Name)) { + Exit-PodeLockable -Object $Object + } + else { + Exit-PodeLockable -Name $Name + } } } } @@ -218,7 +230,7 @@ Enter-PodeLockable -Name 'LockName' -Timeout 5000 function Enter-PodeLockable { [CmdletBinding(DefaultParameterSetName = 'Object')] param( - [Parameter(ValueFromPipeline = $true, ParameterSetName = 'Object')] + [Parameter(ValueFromPipeline = $true, Position = 0, ParameterSetName = 'Object')] [object] $Object, @@ -233,37 +245,52 @@ function Enter-PodeLockable { [switch] $CheckGlobal ) - - # get object by name if set - if (![string]::IsNullOrEmpty($Name)) { - $Object = Get-PodeLockable -Name $Name + begin { + $pipelineItemCount = 0 } - # if object is null, default to global - if ($null -eq $Object) { - $Object = $PodeContext.Threading.Lockables.Global + process { + $pipelineItemCount++ } - # check if value type and throw - if ($Object -is [valuetype]) { - throw 'Cannot lock value types' - } + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + # get object by name if set + if (![string]::IsNullOrEmpty($Name)) { + $Object = Get-PodeLockable -Name $Name + } - # check if null and throw - if ($null -eq $Object) { - throw 'Cannot lock a null object' - } + # if object is null, default to global + if ($null -eq $Object) { + $Object = $PodeContext.Threading.Lockables.Global + } - # check if the global lockable is locked - if ($CheckGlobal) { - Lock-PodeObject -Object $PodeContext.Threading.Lockables.Global -ScriptBlock {} -Timeout $Timeout - } + # check if value type and throw + if ($Object -is [valuetype]) { + # Cannot lock a [ValueType] + throw ($PodeLocale.cannotLockValueTypeExceptionMessage) + } - # attempt to acquire lock - $locked = $false - [System.Threading.Monitor]::TryEnter($Object.SyncRoot, $Timeout, [ref]$locked) - if (!$locked) { - throw 'Failed to acquire lock on object' + # check if null and throw + if ($null -eq $Object) { + # Cannot lock an object that is null + throw ($PodeLocale.cannotLockNullObjectExceptionMessage) + } + + # check if the global lockable is locked + if ($CheckGlobal) { + Lock-PodeObject -Object $PodeContext.Threading.Lockables.Global -ScriptBlock {} -Timeout $Timeout + } + + # attempt to acquire lock + $locked = $false + [System.Threading.Monitor]::TryEnter($Object.SyncRoot, $Timeout, [ref]$locked) + if (!$locked) { + # Failed to acquire a lock on the object + throw ($PodeLocale.failedToAcquireLockExceptionMessage) + } } } @@ -289,7 +316,7 @@ Exit-PodeLockable -Name 'LockName' function Exit-PodeLockable { [CmdletBinding(DefaultParameterSetName = 'Object')] param( - [Parameter(ValueFromPipeline = $true, ParameterSetName = 'Object')] + [Parameter(ValueFromPipeline = $true, Position = 0, ParameterSetName = 'Object')] [object] $Object, @@ -297,30 +324,44 @@ function Exit-PodeLockable { [string] $Name ) - - # get object by name if set - if (![string]::IsNullOrEmpty($Name)) { - $Object = Get-PodeLockable -Name $Name + begin { + $pipelineItemCount = 0 } - # if object is null, default to global - if ($null -eq $Object) { - $Object = $PodeContext.Threading.Lockables.Global + process { + $pipelineItemCount++ } - # check if value type and throw - if ($Object -is [valuetype]) { - throw 'Cannot unlock value types' - } + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + # get object by name if set + if (![string]::IsNullOrEmpty($Name)) { + $Object = Get-PodeLockable -Name $Name + } - # check if null and throw - if ($null -eq $Object) { - throw 'Cannot unlock a null object' - } + # if object is null, default to global + if ($null -eq $Object) { + $Object = $PodeContext.Threading.Lockables.Global + } - if ([System.Threading.Monitor]::IsEntered($Object.SyncRoot)) { - [System.Threading.Monitor]::Pulse($Object.SyncRoot) - [System.Threading.Monitor]::Exit($Object.SyncRoot) + # check if value type and throw + if ($Object -is [valuetype]) { + # Cannot unlock a [ValueType] + throw ($PodeLocale.cannotUnlockValueTypeExceptionMessage) + } + + # check if null and throw + if ($null -eq $Object) { + # Cannot unlock an object that is null + throw ($PodeLocale.cannotUnlockNullObjectExceptionMessage) + } + + if ([System.Threading.Monitor]::IsEntered($Object.SyncRoot)) { + [System.Threading.Monitor]::Pulse($Object.SyncRoot) + [System.Threading.Monitor]::Exit($Object.SyncRoot) + } } } @@ -335,6 +376,7 @@ Remove all Lockables. Clear-PodeLockables #> function Clear-PodeLockables { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param() @@ -386,7 +428,8 @@ function New-PodeMutex { ) if (Test-PodeMutex -Name $Name) { - throw "A mutex with the following name already exists: $($Name)" + # A mutex with the following name already exists + throw ($PodeLocale.mutexAlreadyExistsExceptionMessage -f $Name) } $mutex = $null @@ -574,11 +617,13 @@ function Enter-PodeMutex { $mutex = Get-PodeMutex -Name $Name if ($null -eq $mutex) { - throw "No mutex found called '$($Name)'" + # No mutex found called 'Name' + throw ($PodeLocale.noMutexFoundExceptionMessage -f $Name) } if (!$mutex.WaitOne($Timeout)) { - throw "Failed to acquire mutex ownership. Mutex name: $($Name)" + # Failed to acquire mutex ownership. Mutex name: Name + throw ($PodeLocale.failedToAcquireMutexOwnershipExceptionMessage -f $Name) } } @@ -605,7 +650,8 @@ function Exit-PodeMutex { $mutex = Get-PodeMutex -Name $Name if ($null -eq $mutex) { - throw "No mutex found called '$($Name)'" + # No mutex found called 'Name' + throw ($PodeLocale.noMutexFoundExceptionMessage -f $Name) } $mutex.ReleaseMutex() @@ -622,6 +668,7 @@ Removes all Mutexes. Clear-PodeMutexes #> function Clear-PodeMutexes { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param() @@ -680,7 +727,8 @@ function New-PodeSemaphore { ) if (Test-PodeSemaphore -Name $Name) { - throw "A semaphore with the following name already exists: $($Name)" + # A semaphore with the following name already exists + throw ($PodeLocale.semaphoreAlreadyExistsExceptionMessage -f $Name) } if ($Count -le 0) { @@ -872,11 +920,13 @@ function Enter-PodeSemaphore { $semaphore = Get-PodeSemaphore -Name $Name if ($null -eq $semaphore) { - throw "No semaphore found called '$($Name)'" + # No semaphore found called 'Name' + throw ($PodeLocale.noSemaphoreFoundExceptionMessage -f $Name) } if (!$semaphore.WaitOne($Timeout)) { - throw "Failed to acquire semaphore ownership. Semaphore name: $($Name)" + # Failed to acquire semaphore ownership. Semaphore name: Name + throw ($PodeLocale.failedToAcquireSemaphoreOwnershipExceptionMessage -f $Name) } } @@ -910,7 +960,8 @@ function Exit-PodeSemaphore { $semaphore = Get-PodeSemaphore -Name $Name if ($null -eq $semaphore) { - throw "No semaphore found called '$($Name)'" + # No semaphore found called 'Name' + throw ($PodeLocale.noSemaphoreFoundExceptionMessage -f $Name) } if ($ReleaseCount -lt 1) { @@ -931,6 +982,7 @@ Removes all Semaphores. Clear-PodeSemaphores #> function Clear-PodeSemaphores { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param() diff --git a/src/Public/Timers.ps1 b/src/Public/Timers.ps1 index 84ef29dd7..78a9ecc73 100644 --- a/src/Public/Timers.ps1 +++ b/src/Public/Timers.ps1 @@ -1,45 +1,45 @@ <# .SYNOPSIS -Adds a new Timer with logic to periodically invoke. + Adds a new Timer with logic to periodically invoke. .DESCRIPTION -Adds a new Timer with logic to periodically invoke, with options to only run a specific number of times. + Adds a new Timer with logic to periodically invoke, with options to only run a specific number of times. .PARAMETER Name -The Name of the Timer. + The Name of the Timer. .PARAMETER Interval -The number of seconds to periodically invoke the Timer's ScriptBlock. + The number of seconds to periodically invoke the Timer's ScriptBlock. .PARAMETER ScriptBlock -The script for the Timer. + The script for the Timer. .PARAMETER Limit -The number of times the Timer should be invoked before being removed. (If 0, it will run indefinitely) + The number of times the Timer should be invoked before being removed. (If 0, it will run indefinitely) .PARAMETER Skip -The number of "invokes" to skip before the Timer actually runs. + The number of "invokes" to skip before the Timer actually runs. .PARAMETER ArgumentList -An array of arguments to supply to the Timer's ScriptBlock. + An array of arguments to supply to the Timer's ScriptBlock. .PARAMETER FilePath -A literal, or relative, path to a file containing a ScriptBlock for the Timer's logic. + A literal, or relative, path to a file containing a ScriptBlock for the Timer's logic. .PARAMETER OnStart -If supplied, the timer will trigger when the server starts. + If supplied, the timer will trigger when the server starts. .EXAMPLE -Add-PodeTimer -Name 'Hello' -Interval 10 -ScriptBlock { 'Hello, world!' | Out-Default } + Add-PodeTimer -Name 'Hello' -Interval 10 -ScriptBlock { 'Hello, world!' | Out-Default } .EXAMPLE -Add-PodeTimer -Name 'RunOnce' -Interval 1 -Limit 1 -ScriptBlock { /* logic */ } + Add-PodeTimer -Name 'RunOnce' -Interval 1 -Limit 1 -ScriptBlock { /* logic */ } .EXAMPLE -Add-PodeTimer -Name 'RunAfter60secs' -Interval 10 -Skip 6 -ScriptBlock { /* logic */ } + Add-PodeTimer -Name 'RunAfter60secs' -Interval 10 -Skip 6 -ScriptBlock { /* logic */ } .EXAMPLE -Add-PodeTimer -Name 'Args' -Interval 2 -ScriptBlock { /* logic */ } -ArgumentList 'arg1', 'arg2' + Add-PodeTimer -Name 'Args' -Interval 2 -ScriptBlock { /* logic */ } -ArgumentList 'arg1', 'arg2' #> function Add-PodeTimer { [CmdletBinding(DefaultParameterSetName = 'Script')] @@ -81,22 +81,26 @@ function Add-PodeTimer { # ensure the timer doesn't already exist if ($PodeContext.Timers.Items.ContainsKey($Name)) { - throw "[Timer] $($Name): Timer already defined" + # [Timer] Name: Timer already defined + throw ($PodeLocale.timerAlreadyDefinedExceptionMessage -f $Name) } # is the interval valid? if ($Interval -le 0) { - throw "[Timer] $($Name): Interval must be greater than 0" + # [Timer] Name: parameter must be greater than 0 + throw ($PodeLocale.timerParameterMustBeGreaterThanZeroExceptionMessage -f $Name, 'Interval') } # is the limit valid? if ($Limit -lt 0) { - throw "[Timer] $($Name): Cannot have a negative limit" + # [Timer] Name: parameter must be greater than 0 + throw ($PodeLocale.timerParameterMustBeGreaterThanZeroExceptionMessage -f $Name, 'Limit') } # is the skip valid? if ($Skip -lt 0) { - throw "[Timer] $($Name): Cannot have a negative skip value" + # [Timer] Name: parameter must be greater than 0 + throw ($PodeLocale.timerParameterMustBeGreaterThanZeroExceptionMessage -f $Name, 'Skip') } # if we have a file path supplied, load that path as a scriptblock @@ -151,7 +155,7 @@ Invoke-PodeTimer -Name 'timer-name' function Invoke-PodeTimer { [CmdletBinding()] param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [string] $Name, @@ -159,14 +163,16 @@ function Invoke-PodeTimer { [object[]] $ArgumentList = $null ) - - # ensure the timer exists - if (!$PodeContext.Timers.Items.ContainsKey($Name)) { - throw "Timer '$($Name)' does not exist" + process { + # ensure the timer exists + if (!$PodeContext.Timers.Items.ContainsKey($Name)) { + # Timer 'Name' does not exist + throw ($PodeLocale.timerDoesNotExistExceptionMessage -f $Name) + } + + # run timer logic + Invoke-PodeInternalTimer -Timer $PodeContext.Timers.Items[$Name] -ArgumentList $ArgumentList } - - # run timer logic - Invoke-PodeInternalTimer -Timer $PodeContext.Timers.Items[$Name] -ArgumentList $ArgumentList } <# @@ -185,12 +191,13 @@ Remove-PodeTimer -Name 'SaveState' function Remove-PodeTimer { [CmdletBinding()] param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [string] $Name ) - - $null = $PodeContext.Timers.Items.Remove($Name) + process { + $null = $PodeContext.Timers.Items.Remove($Name) + } } <# @@ -204,6 +211,7 @@ Removes all Timers. Clear-PodeTimers #> function Clear-PodeTimers { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param() @@ -235,7 +243,7 @@ Edit-PodeTimer -Name 'Hello' -Interval 10 function Edit-PodeTimer { [CmdletBinding()] param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [string] $Name, @@ -251,29 +259,31 @@ function Edit-PodeTimer { [object[]] $ArgumentList ) - - # ensure the timer exists - if (!$PodeContext.Timers.Items.ContainsKey($Name)) { - throw "Timer '$($Name)' does not exist" - } - - $_timer = $PodeContext.Timers.Items[$Name] - - # edit interval if supplied - if ($Interval -gt 0) { - $_timer.Interval = $Interval - } - - # edit scriptblock if supplied - if (!(Test-PodeIsEmpty $ScriptBlock)) { - $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState - $_timer.Script = $ScriptBlock - $_timer.UsingVariables = $usingVars - } - - # edit arguments if supplied - if (!(Test-PodeIsEmpty $ArgumentList)) { - $_timer.Arguments = $ArgumentList + process { + # ensure the timer exists + if (!$PodeContext.Timers.Items.ContainsKey($Name)) { + # Timer 'Name' does not exist + throw ($PodeLocale.timerDoesNotExistExceptionMessage -f $Name) + } + + $_timer = $PodeContext.Timers.Items[$Name] + + # edit interval if supplied + if ($Interval -gt 0) { + $_timer.Interval = $Interval + } + + # edit scriptblock if supplied + if (!(Test-PodeIsEmpty $ScriptBlock)) { + $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + $_timer.Script = $ScriptBlock + $_timer.UsingVariables = $usingVars + } + + # edit arguments if supplied + if (!(Test-PodeIsEmpty $ArgumentList)) { + $_timer.Arguments = $ArgumentList + } } } @@ -361,6 +371,7 @@ Use-PodeTimers Use-PodeTimers -Path './my-timers' #> function Use-PodeTimers { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param( [Parameter()] diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index d73fa132f..893ffd464 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -94,22 +94,34 @@ function Start-PodeStopwatch { [string] $Name, - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [scriptblock] $ScriptBlock ) - - try { - $watch = [System.Diagnostics.Stopwatch]::StartNew() - . $ScriptBlock + begin { + $pipelineItemCount = 0 } - catch { - $_ | Write-PodeErrorLog - throw $_.Exception + + process { + $pipelineItemCount++ } - finally { - $watch.Stop() - "[Stopwatch]: $($watch.Elapsed) [$($Name)]" | Out-PodeHost + + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + try { + $watch = [System.Diagnostics.Stopwatch]::StartNew() + . $ScriptBlock + } + catch { + $_ | Write-PodeErrorLog + throw $_.Exception + } + finally { + $watch.Stop() + "[Stopwatch]: $($watch.Elapsed) [$($Name)]" | Out-PodeHost + } } } @@ -180,7 +192,7 @@ function Use-PodeScript { # we have a path, if it's a directory/wildcard then loop over all files if (![string]::IsNullOrWhiteSpace($_path)) { - $_paths = Get-PodeWildcardFiles -Path $Path -Wildcard '*.ps1' + $_paths = Get-PodeWildcardFile -Path $Path -Wildcard '*.ps1' if (!(Test-PodeIsEmpty $_paths)) { foreach ($_path in $_paths) { Use-PodeScript -Path $_path @@ -192,7 +204,8 @@ function Use-PodeScript { # check if the path exists if (!(Test-PodePath $_path -NoStatus)) { - throw "The script path does not exist: $(Protect-PodeValue -Value $_path -Default $Path)" + # The script path does not exist + throw ($PodeLocale.scriptPathDoesNotExistExceptionMessage -f (Protect-PodeValue -Value $_path -Default $Path)) } # dot-source the script @@ -239,7 +252,7 @@ Add-PodeEndware -ScriptBlock { /* logic */ } function Add-PodeEndware { [CmdletBinding()] param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [scriptblock] $ScriptBlock, @@ -247,15 +260,27 @@ function Add-PodeEndware { [object[]] $ArgumentList ) + begin { + $pipelineItemCount = 0 + } - # check for scoped vars - $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + process { + $pipelineItemCount++ + } - # add the scriptblock to array of endware that needs to be run - $PodeContext.Server.Endware += @{ - Logic = $ScriptBlock - UsingVariables = $usingVars - Arguments = $ArgumentList + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + # check for scoped vars + $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + + # add the scriptblock to array of endware that needs to be run + $PodeContext.Server.Endware += @{ + Logic = $ScriptBlock + UsingVariables = $usingVars + Arguments = $ArgumentList + } } } @@ -337,7 +362,7 @@ function Import-PodeModule { 'path' { $Path = Get-PodeRelativePath -Path $Path -RootPath $rootPath -JoinRoot -Resolve - $paths = Get-PodeWildcardFiles -Path $Path -RootPath $rootPath -Wildcard '*.ps*1' + $paths = Get-PodeWildcardFile -Path $Path -RootPath $rootPath -Wildcard '*.ps*1' if (!(Test-PodeIsEmpty $paths)) { foreach ($_path in $paths) { Import-PodeModule -Path $_path @@ -350,12 +375,14 @@ function Import-PodeModule { # if it's still empty, error if ([string]::IsNullOrWhiteSpace($Path)) { - throw "Failed to import module: $(Protect-PodeValue -Value $Path -Default $Name)" + # Failed to import module + throw ($PodeLocale.failedToImportModuleExceptionMessage -f (Protect-PodeValue -Value $Path -Default $Name)) } # check if the path exists if (!(Test-PodePath $Path -NoStatus)) { - throw "The module path does not exist: $(Protect-PodeValue -Value $Path -Default $Name)" + # The module path does not exist + throw ($PodeLocale.modulePathDoesNotExistExceptionMessage -f (Protect-PodeValue -Value $Path -Default $Name)) } $null = Import-Module $Path -Force -DisableNameChecking -Scope Global -ErrorAction Stop @@ -384,7 +411,8 @@ function Import-PodeSnapin { # if non-windows or core, fail if ((Test-PodeIsPSCore) -or (Test-PodeIsUnix)) { - throw 'Snapins are only supported on Windows PowerShell' + # Snapins are only supported on Windows PowerShell + throw ($PodeLocale.snapinsSupportedOnWindowsPowershellOnlyExceptionMessage) } # import the snap-in @@ -491,7 +519,7 @@ Spat the argument onto the ScriptBlock. Don't create a new closure before invoking the ScriptBlock. .EXAMPLE -Invoke-PodeScriptBlock -ScriptBlock { Write-Host 'Hello!' } +Invoke-PodeScriptBlock -ScriptBlock { Write-PodeHost 'Hello!' } .EXAMPLE Invoke-PodeScriptBlock -Arguments 'Morty' -ScriptBlock { /* logic */ } @@ -584,6 +612,9 @@ $Arguments = @(Merge-PodeScriptblockArguments -ArgumentList $Arguments -UsingVar $Arguments = @(Merge-PodeScriptblockArguments -UsingVariables $UsingVariables) #> function Merge-PodeScriptblockArguments { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] + [CmdletBinding()] + [OutputType([object[]])] param( [Parameter()] [object[]] @@ -768,14 +799,34 @@ The object to output. function Out-PodeHost { [CmdletBinding()] param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [object] $InputObject ) + begin { + # Initialize an array to hold piped-in values + $pipelineValue = @() + } + + process { + # Add the current piped-in value to the array + $pipelineValue += $_ + } - if (!$PodeContext.Server.Quiet) { - $InputObject | Out-Default + end { + if ($PodeContext.Server.Quiet) { + return + } + # Set InputObject to the array of values + if ($pipelineValue.Count -gt 1) { + $InputObject = $pipelineValue + $InputObject | Out-Default + } + else { + Out-Default -InputObject $InputObject + } } + } <# @@ -801,10 +852,14 @@ Show the object content .PARAMETER ShowType Show the Object Type +.PARAMETER Label +Show a label for the object + .EXAMPLE 'Some output' | Write-PodeHost -ForegroundColor Cyan #> function Write-PodeHost { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] [CmdletBinding(DefaultParameterSetName = 'inbuilt')] param( [Parameter(Position = 0, ValueFromPipeline = $true)] @@ -824,34 +879,67 @@ function Write-PodeHost { [Parameter( Mandatory = $false, ParameterSetName = 'object')] [switch] - $ShowType + $ShowType, + + [Parameter( Mandatory = $false, ParameterSetName = 'object')] + [string] + $Label ) + begin { + # Initialize an array to hold piped-in values + $pipelineValue = @() + } - if ($PodeContext.Server.Quiet) { - return + process { + # Add the current piped-in value to the array + $pipelineValue += $_ } - if ($Explode.IsPresent ) { - if ($null -eq $Object) { - if ($ShowType) { - $Object = "`tNull Value" + end { + if ($PodeContext.Server.Quiet) { + return + } + # Set Object to the array of values + if ($pipelineValue.Count -gt 1) { + $Object = $pipelineValue + } + + if ($Explode.IsPresent ) { + if ($null -eq $Object) { + if ($ShowType) { + $Object = "`tNull Value" + } + } + else { + $type = $Object.gettype().FullName + $Object = $Object | Out-String + if ($ShowType) { + $Object = "`tTypeName: $type`n$Object" + } + } + if ($Label) { + $Object = "`tName: $Label $Object" + } + + } + + if ($ForegroundColor) { + if ($pipelineValue.Count -gt 1) { + $Object | Write-Host -ForegroundColor $ForegroundColor -NoNewline:$NoNewLine + } + else { + Write-Host -Object $Object -ForegroundColor $ForegroundColor -NoNewline:$NoNewLine } } else { - $type = $Object.gettype().FullName - $Object = $Object | Out-String - if ($ShowType) { - $Object = "`tTypeName: $type`n$Object" + if ($pipelineValue.Count -gt 1) { + $Object | Write-Host -NoNewline:$NoNewLine + } + else { + Write-Host -Object $Object -NoNewline:$NoNewLine } } } - - if ($ForegroundColor) { - Write-Host -Object $Object -ForegroundColor $ForegroundColor -NoNewline:$NoNewLine - } - else { - Write-Host -Object $Object -NoNewline:$NoNewLine - } } <# @@ -949,12 +1037,28 @@ function Out-PodeVariable { [string] $Name, - [Parameter(ValueFromPipeline = $true)] + [Parameter(Position = 0, ValueFromPipeline = $true)] [object] $Value ) + begin { + # Initialize an array to hold piped-in values + $pipelineValue = @() + } - $PodeContext.Server.Output.Variables[$Name] = $Value + process { + # Add the current piped-in value to the array + $pipelineValue += $_ + } + + end { + # Set Value to the array of values + if ($pipelineValue.Count -gt 1) { + $Value = $pipelineValue + } + + $PodeContext.Server.Output.Variables[$Name] = $Value + } } <# @@ -1056,7 +1160,8 @@ function New-PodeCron { # cant have None and Interval if (($Every -ieq 'none') -and ($Interval -gt 0)) { - throw 'Cannot supply an interval when -Every is set to None' + # Cannot supply an interval when the parameter `Every` is set to None + throw ($PodeLocale.cannotSupplyIntervalWhenEveryIsNoneExceptionMessage) } # base cron @@ -1156,7 +1261,8 @@ function New-PodeCron { $cron.Month = '1,4,7,10' if ($Interval -gt 0) { - throw 'Cannot supply interval value for every quarter' + # Cannot supply interval value for every quarter + throw ($PodeLocale.cannotSupplyIntervalForQuarterExceptionMessage) } } @@ -1167,7 +1273,8 @@ function New-PodeCron { $cron.Month = '1' if ($Interval -gt 0) { - throw 'Cannot supply interval value for every year' + # Cannot supply interval value for every year + throw ($PodeLocale.cannotSupplyIntervalForYearExceptionMessage) } } } @@ -1274,86 +1381,108 @@ Outputs an ordered hashtable representing the XML node structure. .NOTES This cmdlet is useful for transforming XML data into a structure that's easier to manipulate in PowerShell scripts. - -.LINK -https://badgerati.github.io/Pode/Functions/Utility/ConvertFrom-PodeXml - #> function ConvertFrom-PodeXml { [CmdletBinding()] [OutputType([System.Collections.Specialized.OrderedDictionary])] param ( - [Parameter(Mandatory = $true, ValueFromPipeline)] - [System.Xml.XmlNode]$node, #we are working through the nodes - [string]$Prefix = '', #do we indicate an attribute with a prefix? - $ShowDocElement = $false, #Do we show the document element?, + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] + [System.Xml.XmlNode]$node, + + [Parameter()] + [string] + $Prefix = '', + + [Parameter()] + [switch] + $ShowDocElement, + + [Parameter()] [switch] $KeepAttributes ) - #if option set, we skip the Document element - if ($node.DocumentElement -and !($ShowDocElement)) - { $node = $node.DocumentElement } - $oHash = [ordered] @{ } # start with an ordered hashtable. - #The order of elements is always significant regardless of what they are - if ($null -ne $node.Attributes ) { - #if there are elements - # record all the attributes first in the ordered hash - $node.Attributes | ForEach-Object { - $oHash.$("$Prefix$($_.FirstChild.parentNode.LocalName)") = $_.FirstChild.value - } - } - # check to see if there is a pseudo-array. (more than one - # child-node with the same name that must be handled as an array) - $node.ChildNodes | #we just group the names and create an empty - #array for each - Group-Object -Property LocalName | Where-Object { $_.count -gt 1 } | Select-Object Name | - ForEach-Object { - $oHash.($_.Name) = @() <# create an empty array for each one#> - } - foreach ($child in $node.ChildNodes) { - #now we look at each node in turn. - $childName = $child.LocalName - if ($child -is [system.xml.xmltext]) { - # if it is simple XML text - $oHash.$childname += $child.InnerText + process { + #if option set, we skip the Document element + if ($node.DocumentElement -and !($ShowDocElement.IsPresent)) + { $node = $node.DocumentElement } + $oHash = [ordered] @{ } # start with an ordered hashtable. + #The order of elements is always significant regardless of what they are + if ($null -ne $node.Attributes ) { + #if there are elements + # record all the attributes first in the ordered hash + $node.Attributes | ForEach-Object { + $oHash.$("$Prefix$($_.FirstChild.parentNode.LocalName)") = $_.FirstChild.value + } } - # if it has a #text child we may need to cope with attributes - elseif ($child.FirstChild.Name -eq '#text' -and $child.ChildNodes.Count -eq 1) { - if ($null -ne $child.Attributes -and $KeepAttributes ) { - #hah, an attribute - <#we need to record the text with the #text label and preserve all + # check to see if there is a pseudo-array. (more than one + # child-node with the same name that must be handled as an array) + $node.ChildNodes | #we just group the names and create an empty + #array for each + Group-Object -Property LocalName | Where-Object { $_.count -gt 1 } | Select-Object Name | + ForEach-Object { + $oHash.($_.Name) = @() <# create an empty array for each one#> + } + foreach ($child in $node.ChildNodes) { + #now we look at each node in turn. + $childName = $child.LocalName + if ($child -is [system.xml.xmltext]) { + # if it is simple XML text + $oHash.$childname += $child.InnerText + } + # if it has a #text child we may need to cope with attributes + elseif ($child.FirstChild.Name -eq '#text' -and $child.ChildNodes.Count -eq 1) { + if ($null -ne $child.Attributes -and $KeepAttributes ) { + #hah, an attribute + <#we need to record the text with the #text label and preserve all the attributes #> - $aHash = [ordered]@{ } - $child.Attributes | ForEach-Object { - $aHash.$($_.FirstChild.parentNode.LocalName) = $_.FirstChild.value + $aHash = [ordered]@{ } + $child.Attributes | ForEach-Object { + $aHash.$($_.FirstChild.parentNode.LocalName) = $_.FirstChild.value + } + #now we add the text with an explicit name + $aHash.'#text' += $child.'#text' + $oHash.$childname += $aHash + } + else { + #phew, just a simple text attribute. + $oHash.$childname += $child.FirstChild.InnerText } - #now we add the text with an explicit name - $aHash.'#text' += $child.'#text' - $oHash.$childname += $aHash } - else { - #phew, just a simple text attribute. - $oHash.$childname += $child.FirstChild.InnerText + elseif ($null -ne $child.'#cdata-section' ) { + # if it is a data section, a block of text that isnt parsed by the parser, + # but is otherwise recognized as markup + $oHash.$childname = $child.'#cdata-section' } - } - elseif ($null -ne $child.'#cdata-section' ) { - # if it is a data section, a block of text that isnt parsed by the parser, - # but is otherwise recognized as markup - $oHash.$childname = $child.'#cdata-section' - } - elseif ($child.ChildNodes.Count -gt 1 -and + elseif ($child.ChildNodes.Count -gt 1 -and ($child | Get-Member -MemberType Property).Count -eq 1) { - $oHash.$childname = @() - foreach ($grandchild in $child.ChildNodes) { - $oHash.$childname += (ConvertFrom-PodeXml $grandchild) + $oHash.$childname = @() + foreach ($grandchild in $child.ChildNodes) { + $oHash.$childname += (ConvertFrom-PodeXml $grandchild) + } + } + else { + # create an array as a value to the hashtable element + $oHash.$childname += (ConvertFrom-PodeXml $child) } } - else { - # create an array as a value to the hashtable element - $oHash.$childname += (ConvertFrom-PodeXml $child) - } + return $oHash } - return $oHash +} + +<# +.SYNOPSIS +Invokes the garbage collector. + +.DESCRIPTION +Invokes the garbage collector. -} \ No newline at end of file +.EXAMPLE +Invoke-PodeGC +#> +function Invoke-PodeGC { + [CmdletBinding()] + param() + + [System.GC]::Collect() +} diff --git a/src/Public/Verbs.ps1 b/src/Public/Verbs.ps1 index 63cd58b9c..3adf26557 100644 --- a/src/Public/Verbs.ps1 +++ b/src/Public/Verbs.ps1 @@ -70,10 +70,10 @@ function Add-PodeVerb { ) # find placeholder parameters in verb (ie: COMMAND :parameter) - $Verb = Resolve-PodePlaceholders -Path $Verb + $Verb = Resolve-PodePlaceholder -Path $Verb # get endpoints from name - $endpoints = Find-PodeEndpoints -EndpointName $EndpointName + $endpoints = Find-PodeEndpoint -EndpointName $EndpointName # ensure the verb doesn't already exist for each endpoint foreach ($_endpoint in $endpoints) { @@ -82,7 +82,8 @@ function Add-PodeVerb { # if scriptblock and file path are all null/empty, error if ((Test-PodeIsEmpty $ScriptBlock) -and (Test-PodeIsEmpty $FilePath) -and !$Close -and !$UpgradeToSsl) { - throw "[Verb] $($Verb): No logic passed" + # [Verb] Verb: No logic passed + throw ($PodeLocale.verbNoLogicPassedExceptionMessage -f $Verb) } # if we have a file path supplied, load that path as a scriptblock @@ -146,7 +147,7 @@ function Remove-PodeVerb { ) # ensure the verb placeholders are replaced - $Verb = Resolve-PodePlaceholders -Path $Verb + $Verb = Resolve-PodePlaceholder -Path $Verb # ensure verb does exist if (!$PodeContext.Server.Verbs.Contains($Verb)) { @@ -175,6 +176,7 @@ Removes all added Verbs. Clear-PodeVerbs #> function Clear-PodeVerbs { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param() @@ -217,7 +219,7 @@ function Get-PodeVerb { # if we have a verb, filter if (![string]::IsNullOrWhiteSpace($Verb)) { - $Verb = Resolve-PodePlaceholders -Path $Verb + $Verb = Resolve-PodePlaceholder -Path $Verb $verbs = $PodeContext.Server.Verbs[$Verb] } else { @@ -260,6 +262,7 @@ Use-PodeVerbs Use-PodeVerbs -Path './my-verbs' #> function Use-PodeVerbs { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param( [Parameter()] diff --git a/src/Public/WebSockets.ps1 b/src/Public/WebSockets.ps1 index 5d2cbd21d..e1d9f4958 100644 --- a/src/Public/WebSockets.ps1 +++ b/src/Public/WebSockets.ps1 @@ -23,7 +23,9 @@ function Set-PodeWebSocketConcurrency { # error if <=0 if ($Maximum -le 0) { - throw "Maximum concurrent WebSocket threads must be >=1 but got: $($Maximum)" + # Maximum concurrent WebSocket threads must be >=1 but got + throw ($PodeLocale.maximumConcurrentWebSocketThreadsInvalidExceptionMessage -f $Maximum) + } # add 1, for the waiting script @@ -36,7 +38,8 @@ function Set-PodeWebSocketConcurrency { } if ($_min -gt $Maximum) { - throw "Maximum concurrent WebSocket threads cannot be less than the minimum of $($_min) but got: $($Maximum)" + # Maximum concurrent WebSocket threads cannot be less than the minimum of $_min but got $Maximum + throw ($PodeLocale.maximumConcurrentWebSocketThreadsLessThanMinimumExceptionMessage -f $_min, $Maximum) } # set the max tasks @@ -116,7 +119,8 @@ function Connect-PodeWebSocket { # fail if already exists if (Test-PodeWebSocket -Name $Name) { - throw "Already connected to websocket with name '$($Name)'" + # Already connected to websocket with name + throw ($PodeLocale.alreadyConnectedToWebSocketExceptionMessage -f $Name) } # if we have a file path supplied, load that path as a scriptblock @@ -129,10 +133,11 @@ function Connect-PodeWebSocket { # connect try { - $PodeContext.Server.WebSockets.Receiver.ConnectWebSocket($Name, $Url, $ContentType) + $null = Wait-PodeTask -Task $PodeContext.Server.WebSockets.Receiver.ConnectWebSocket($Name, $Url, $ContentType) } catch { - throw "Failed to connect to websocket: $($_.Exception.Message)" + # Failed to connect to websocket + throw ($PodeLocale.failedToConnectToWebSocketExceptionMessage -f $ErrorMessage) } $PodeContext.Server.WebSockets.Connections[$Name] = @{ @@ -170,7 +175,8 @@ function Disconnect-PodeWebSocket { } if ([string]::IsNullOrWhiteSpace($Name)) { - throw 'No Name for a WebSocket to disconnect from supplied' + # No Name for a WebSocket to disconnect from supplied + throw ($PodeLocale.noNameForWebSocketDisconnectExceptionMessage) } if (Test-PodeWebSocket -Name $Name) { @@ -204,7 +210,8 @@ function Remove-PodeWebSocket { } if ([string]::IsNullOrWhiteSpace($Name)) { - throw 'No Name for a WebSocket to remove supplied' + # No Name for a WebSocket to remove supplied + throw ($PodeLocale.noNameForWebSocketRemoveExceptionMessage) } $PodeContext.Server.WebSockets.Receiver.RemoveWebSocket($Name) @@ -260,7 +267,8 @@ function Send-PodeWebSocket { # do we have a name? if ([string]::IsNullOrWhiteSpace($Name)) { - throw 'No Name for a WebSocket to send message to supplied' + # No Name for a WebSocket to send message to supplied + throw ($PodeLocale.noNameForWebSocketSendMessageExceptionMessage) } # do the socket exist? @@ -275,7 +283,7 @@ function Send-PodeWebSocket { $Message = ConvertTo-PodeResponseContent -InputObject $Message -ContentType $ws.ContentType -Depth $Depth # send message - $ws.Send($Message, $Type) + $null = Wait-PodeTask -Task $ws.Send($Message, $Type) } <# @@ -310,16 +318,17 @@ function Reset-PodeWebSocket { ) if ([string]::IsNullOrWhiteSpace($Name) -and ($null -ne $WsEvent)) { - $WsEvent.Request.WebSocket.Reconnect($Url) + $null = Wait-PodeTask -Task $WsEvent.Request.WebSocket.Reconnect($Url) return } if ([string]::IsNullOrWhiteSpace($Name)) { - throw 'No Name for a WebSocket to reset supplied' + # No Name for a WebSocket to reset supplied + throw ($PodeLocale.noNameForWebSocketResetExceptionMessage) } if (Test-PodeWebSocket -Name $Name) { - $PodeContext.Server.WebSockets.Receiver.GetWebSocket($Name).Reconnect($Url) + $null = Wait-PodeTask -Task $PodeContext.Server.WebSockets.Receiver.GetWebSocket($Name).Reconnect($Url) } } @@ -338,6 +347,7 @@ Test-PodeWebSocket -Name 'Example' #> function Test-PodeWebSocket { [CmdletBinding()] + [OutputType([bool])] param( [Parameter(Mandatory = $true)] [string] diff --git a/tests/integration/Authentication.Tests.ps1 b/tests/integration/Authentication.Tests.ps1 index 67344ad67..67f4147c3 100644 --- a/tests/integration/Authentication.Tests.ps1 +++ b/tests/integration/Authentication.Tests.ps1 @@ -1,13 +1,11 @@ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseUsingScopeModifierInNewRunspaces', '')] param() - BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]integration', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } } - Describe 'Authentication Requests' { BeforeAll { diff --git a/tests/integration/OpenApi.Tests.ps1 b/tests/integration/OpenApi.Tests.ps1 new file mode 100644 index 000000000..62cf921ad --- /dev/null +++ b/tests/integration/OpenApi.Tests.ps1 @@ -0,0 +1,204 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseUsingScopeModifierInNewRunspaces', '')] +param() + +Describe 'OpenAPI integration tests' { + + BeforeAll { + $mindyCommonHeaders = @{ + 'accept' = 'application/json' + 'X-API-KEY' = 'test2-api-key' + 'Authorization' = 'Basic bWluZHk6cGlja2xl' + } + + $mortyCommonHeaders = @{ + 'accept' = 'application/json' + 'X-API-KEY' = 'test-api-key' + 'Authorization' = 'Basic bW9ydHk6cGlja2xl' + } + $PortV3 = 8080 + $PortV3_1 = 8081 + $scriptPath = "$($PSScriptRoot)\..\..\examples\OpenApi-TuttiFrutti.ps1" + if ($PSVersionTable.PsVersion -gt [version]'6.0') { + Start-Process 'pwsh' -ArgumentList "-NoProfile -File `"$scriptPath`" -Quiet -PortV3 $PortV3 -PortV3_1 $PortV3_1 -DisableTermination" -NoNewWindow + } + else { + Start-Process 'powershell' -ArgumentList "-NoProfile -File `"$scriptPath`" -Quiet -PortV3 $PortV3 -PortV3_1 $PortV3_1 -DisableTermination" -NoNewWindow + } + + function Compare-StringRnLn { + param ( + [string]$InputString1, + [string]$InputString2 + ) + return ($InputString1.Trim() -replace "`r`n|`n|`r", "`n") -eq ($InputString2.Trim() -replace "`r`n|`n|`r", "`n") + } + + function Convert-PsCustomObjectToOrderedHashtable { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [PSCustomObject]$InputObject + ) + begin { + # Define a recursive function within the process block + function Convert-ObjectRecursively { + param ( + [Parameter(Mandatory = $true)] + [System.Object] + $InputObject + ) + + # Initialize an ordered dictionary + $orderedHashtable = [ordered]@{} + + # Loop through each property of the PSCustomObject + foreach ($property in $InputObject.PSObject.Properties) { + # Check if the property value is a PSCustomObject + if ($property.Value -is [PSCustomObject]) { + # Recursively convert the nested PSCustomObject + $orderedHashtable[$property.Name] = Convert-ObjectRecursively -InputObject $property.Value + } + elseif ($property.Value -is [System.Collections.IEnumerable] -and -not ($property.Value -is [string])) { + # If the value is a collection, check each element + $convertedCollection = @() + foreach ($item in $property.Value) { + if ($item -is [PSCustomObject]) { + $convertedCollection += Convert-ObjectRecursively -InputObject $item + } + else { + $convertedCollection += $item + } + } + $orderedHashtable[$property.Name] = $convertedCollection + } + else { + # Add the property name and value to the ordered hashtable + $orderedHashtable[$property.Name] = $property.Value + } + } + + # Return the resulting ordered hashtable + return $orderedHashtable + } + } + process { + # Call the recursive helper function for each input object + Convert-ObjectRecursively -InputObject $InputObject + } + } + + function Compare-Hashtable { + param ( + [object]$Hashtable1, + [object]$Hashtable2 + ) + + # Function to compare two hashtable values + function Compare-Value($value1, $value2) { + # Check if both values are hashtables + if ((($value1 -is [hashtable] -or $value1 -is [System.Collections.Specialized.OrderedDictionary]) -and + ($value2 -is [hashtable] -or $value2 -is [System.Collections.Specialized.OrderedDictionary]))) { + return Compare-Hashtable -Hashtable1 $value1 -Hashtable2 $value2 + } + # Check if both values are arrays + elseif (($value1 -is [Object[]]) -and ($value2 -is [Object[]])) { + if ($value1.Count -ne $value2.Count) { + return $false + } + for ($i = 0; $i -lt $value1.Count; $i++) { + $found = $false + for ($j = 0; $j -lt $value2.Count; $j++) { + if ( Compare-Value $value1[$i] $value2[$j]) { + $found = $true + } + } + if ($found -eq $false) { + return $false + } + } + return $true + } + else { + if($value1 -is [string] -and $value2 -is [string]){ + return Compare-StringRnLn $value1 $value2 + } + # Check if the values are equal + return $value1 -eq $value2 + } + } + + $keys1 = $Hashtable1.Keys + $keys2 = $Hashtable2.Keys + + # Check if both hashtables have the same keys + if ($keys1.Count -ne $keys2.Count) { + return $false + } + + foreach ($key in $keys1) { + if (! ($Hashtable2.Keys -contains $key)) { + return $false + } + + if ($Hashtable2[$key] -is [hashtable] -or $Hashtable2[$key] -is [System.Collections.Specialized.OrderedDictionary]) { + if (! (Compare-Hashtable -Hashtable1 $Hashtable1[$key] -Hashtable2 $Hashtable2[$key])) { + return $false + } + } + elseif (!(Compare-Value $Hashtable1[$key] $Hashtable2[$key])) { + return $false + } + } + + return $true + } + + Start-Sleep -Seconds 5 + } + + AfterAll { + Start-Sleep -Seconds 5 + Invoke-RestMethod -Uri "http://localhost:$($PortV3)/close" -Method Post | Out-Null + + } + + Describe 'OpenAPI' { + it 'Open API v3.0.3' { + + Start-Sleep -Seconds 10 + $fileContent = Get-Content -Path "$PSScriptRoot/specs/OpenApi-TuttiFrutti_3.0.3.json" + + $webResponse = Invoke-WebRequest -Uri "http://localhost:$($PortV3)/docs/openapi/v3.0" -Method Get + $json = $webResponse.Content + if ( $PSVersionTable.PSEdition -eq 'Desktop') { + $expected = $fileContent | ConvertFrom-Json | Convert-PsCustomObjectToOrderedHashtable + $response = $json | ConvertFrom-Json | Convert-PsCustomObjectToOrderedHashtable + } + else { + $expected = $fileContent | ConvertFrom-Json -AsHashtable + $response = $json | ConvertFrom-Json -AsHashtable + } + + Compare-Hashtable $response $expected | Should -BeTrue + + } + + it 'Open API v3.1.0' { + $fileContent = Get-Content -Path "$PSScriptRoot/specs/OpenApi-TuttiFrutti_3.1.0.json" + + $webResponse = Invoke-WebRequest -Uri "http://localhost:$($PortV3_1)/docs/openapi/v3.1" -Method Get + $json = $webResponse.Content + if ( $PSVersionTable.PSEdition -eq 'Desktop') { + $expected = $fileContent | ConvertFrom-Json | Convert-PsCustomObjectToOrderedHashtable + $response = $json | ConvertFrom-Json | Convert-PsCustomObjectToOrderedHashtable + } + else { + $expected = $fileContent | ConvertFrom-Json -AsHashtable + $response = $json | ConvertFrom-Json -AsHashtable + } + Compare-Hashtable $response $expected | Should -BeTrue + } + } + +} \ No newline at end of file diff --git a/tests/integration/RestApi.Https.Tests.ps1 b/tests/integration/RestApi.Https.Tests.ps1 index 3a54273e6..c40e898cc 100644 --- a/tests/integration/RestApi.Https.Tests.ps1 +++ b/tests/integration/RestApi.Https.Tests.ps1 @@ -5,46 +5,32 @@ param() Describe 'REST API Requests' { BeforeAll { $splatter = @{} - $UseCurl = $true $version = $PSVersionTable.PSVersion - if ( $version.Major -eq 5) { + $useCurl = $false + + if ($version.Major -eq 5) { # Ignore SSL certificate validation errors Add-Type @' -using System.Net; -using System.Security.Cryptography.X509Certificates; -public class TrustAllCertsPolicy : ICertificatePolicy { -public bool CheckValidationResult( - ServicePoint srvPoint, X509Certificate certificate, - WebRequest request, int certificateProblem) { - return true; -} -} + using System.Net; + using System.Security.Cryptography.X509Certificates; + public class TrustAllCertsPolicy : ICertificatePolicy { + public bool CheckValidationResult( + ServicePoint srvPoint, X509Certificate certificate, + WebRequest request, int certificateProblem) { + return true; + } + } '@ - - [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy - $UseCurl = $false + [System.Net.ServicePointManager]::CertificatePolicy = [TrustAllCertsPolicy]::new() } - elseif ($PSVersionTable.OS -like '*Windows*') { - # OS check passed, now check PowerShell version - # Split version by '.' and compare major and minor version - if ( $version.Major -gt 7 -or ($version.Major -eq 7 -and $version.Minor -ge 4)) { - # Running on Windows with PowerShell Core 7.4 or greater. - $UseCurl = $true - } - else { - $UseCurl = $false - $splatter.SkipCertificateCheck = $true - # Running on Windows but with PowerShell version less than 7.4. + else { + if ($version -ge [version]'7.4.0') { + $useCurl = $true } - } - else { - # Not running on Windows." - $UseCurl = $false $splatter.SkipCertificateCheck = $true } - $Port = 8080 $Endpoint = "https://127.0.0.1:$($Port)" @@ -136,135 +122,154 @@ public bool CheckValidationResult( AfterAll { Receive-Job -Name 'Pode' | Out-Default - if ($UseCurl) { - curl -s -X DELETE "$($Endpoint)/close" -k - } - else { - Invoke-RestMethod -Uri "$($Endpoint)/close" -Method Get @splatter | Out-Null - } + Invoke-RestMethod -Uri "$($Endpoint)/close" -Method Get @splatter | Out-Null Get-Job -Name 'Pode' | Remove-Job -Force } It 'responds back with pong' { - if ($UseCurl) { + # test curl + if ($useCurl) { $result = (curl -s -X GET "$($Endpoint)/ping" -k) | ConvertFrom-Json + $result.Result | Should -Be 'Pong' } - else { - $result = Invoke-RestMethod -Uri "$($Endpoint)/ping" -Method Get @splatter - } + + # test Invoke-RestMethod + $result = Invoke-RestMethod -Uri "$($Endpoint)/ping" -Method Get @splatter $result.Result | Should -Be 'Pong' } It 'responds back with 404 for invalid route' { - if ($UseCurl) { + # test curl + if ($useCurl) { $status_code = (curl -s -o /dev/null -w '%{http_code}' "$Endpoint/eek" -k) $status_code | Should -be 404 } - else { - { Invoke-RestMethod -Uri "$($Endpoint)/eek" -Method Get -ErrorAction Stop @splatter } | Should -Throw -ExpectedMessage '*404*' - } + + # test Invoke-RestMethod + { Invoke-RestMethod -Uri "$($Endpoint)/eek" -Method Get -ErrorAction Stop @splatter } | Should -Throw -ExpectedMessage '*404*' } It 'responds back with 405 for incorrect method' { - if ($UseCurl) { + # test curl + if ($useCurl) { $status_code = (curl -X POST -s -o /dev/null -w '%{http_code}' "$Endpoint/ping" -k) $status_code | Should -be 405 } - else { - { Invoke-RestMethod -Uri "$($Endpoint)/ping" -Method Post -ErrorAction Stop @splatter } | Should -Throw -ExpectedMessage '*405*' - } + + # test Invoke-RestMethod + { Invoke-RestMethod -Uri "$($Endpoint)/ping" -Method Post -ErrorAction Stop @splatter } | Should -Throw -ExpectedMessage '*405*' } It 'responds with simple query parameter' { - if ($UseCurl) { + # test curl + if ($useCurl) { $result = (curl -s -X GET "$($Endpoint)/data/query?username=rick" -k) | ConvertFrom-Json + $result.Username | Should -Be 'rick' } - else { - $result = Invoke-RestMethod -Uri "$($Endpoint)/data/query?username=rick" -Method Get @splatter - } + + # test Invoke-RestMethod + $result = Invoke-RestMethod -Uri "$($Endpoint)/data/query?username=rick" -Method Get @splatter $result.Username | Should -Be 'rick' } It 'responds with simple payload parameter - json' { - if ($UseCurl) { + # test curl + if ($useCurl) { $result = curl -s -X POST "$($Endpoint)/data/payload" -H 'Content-Type: application/json' -d '{"username":"rick"}' -k | ConvertFrom-Json + $result.Username | Should -Be 'rick' } - else { - $result = Invoke-RestMethod -Uri "$($Endpoint)/data/payload" -Method Post -Body '{"username":"rick"}' -ContentType 'application/json' @splatter - } + + # test Invoke-RestMethod + $result = Invoke-RestMethod -Uri "$($Endpoint)/data/payload" -Method Post -Body '{"username":"rick"}' -ContentType 'application/json' @splatter $result.Username | Should -Be 'rick' } It 'responds with simple payload parameter - xml' { - if ($UseCurl) { - $result = curl -s -X POST "$($Endpoint)/data/payload" -H 'Content-Type: text/xml' -d 'rick' -k | ConvertFrom-Json - } - else { - $result = Invoke-RestMethod -Uri "$($Endpoint)/data/payload" -Method Post -Body 'rick' -ContentType 'text/xml' @splatter + # test curl + if ($useCurl) { + $result = curl -s -X POST "$($Endpoint)/data/payload" -H 'Content-Type: application/xml' -d 'rick' -k | ConvertFrom-Json + $result.Username | Should -Be 'rick' } + + # test Invoke-RestMethod + $result = Invoke-RestMethod -Uri "$($Endpoint)/data/payload" -Method Post -Body 'rick' -ContentType 'application/xml' @splatter $result.Username | Should -Be 'rick' } It 'responds with simple payload parameter forced to json' { - if ($UseCurl) { + # test curl + if ($useCurl) { $result = curl -s -X POST "$($Endpoint)/data/payload-forced-type" -d '{"username":"rick"}' -k | ConvertFrom-Json + $result.Username | Should -Be 'rick' } - else { - $result = Invoke-RestMethod -Uri "$($Endpoint)/data/payload-forced-type" -Method Post -Body '{"username":"rick"}' @splatter - } + + # test Invoke-RestMethod + $result = Invoke-RestMethod -Uri "$($Endpoint)/data/payload-forced-type" -Method Post -Body '{"username":"rick"}' @splatter $result.Username | Should -Be 'rick' } It 'responds with simple route parameter' { - if ($UseCurl) { + # test curl + if ($useCurl) { $result = (curl -s -X GET "$($Endpoint)/data/param/rick" -k) | ConvertFrom-Json + $result.Username | Should -Be 'rick' } - else { - $result = Invoke-RestMethod -Uri "$($Endpoint)/data/param/rick" -Method Get @splatter - } + + # test Invoke-RestMethod + $result = Invoke-RestMethod -Uri "$($Endpoint)/data/param/rick" -Method Get @splatter $result.Username | Should -Be 'rick' } It 'responds with simple route parameter long' { - if ($UseCurl) { + # test curl + if ($useCurl) { $result = (curl -s -X GET "$($Endpoint)/data/param/rick/messages" -k) | ConvertFrom-Json + $result.Messages[0] | Should -Be 'Hello, world!' + $result.Messages[1] | Should -Be 'Greetings' + $result.Messages[2] | Should -Be 'Wubba Lub' } - else { - $result = Invoke-RestMethod -Uri "$($Endpoint)/data/param/rick/messages" -Method Get @splatter - } + + # test Invoke-RestMethod + $result = Invoke-RestMethod -Uri "$($Endpoint)/data/param/rick/messages" -Method Get @splatter $result.Messages[0] | Should -Be 'Hello, world!' $result.Messages[1] | Should -Be 'Greetings' $result.Messages[2] | Should -Be 'Wubba Lub' } It 'responds ok to remove account' { - if ($UseCurl) { + # test curl + if ($useCurl) { $result = (curl -s -X DELETE "$($Endpoint)/api/rick/remove" -k) | ConvertFrom-Json + $result.Result | Should -Be 'OK' } - else { - $result = Invoke-RestMethod -Uri "$($Endpoint)/api/rick/remove" -Method Delete @splatter - } + + # test Invoke-RestMethod + $result = Invoke-RestMethod -Uri "$($Endpoint)/api/rick/remove" -Method Delete @splatter $result.Result | Should -Be 'OK' } It 'responds ok to replace account' { - if ($UseCurl) { + # test curl + if ($useCurl) { $result = (curl -s -X PUT "$($Endpoint)/api/rick/replace" -k) | ConvertFrom-Json + $result.Result | Should -Be 'OK' } - else { - $result = Invoke-RestMethod -Uri "$($Endpoint)/api/rick/replace" -Method Put @splatter - } + + # test Invoke-RestMethod + $result = Invoke-RestMethod -Uri "$($Endpoint)/api/rick/replace" -Method Put @splatter $result.Result | Should -Be 'OK' } It 'responds ok to update account' { - if ($UseCurl) { + # test curl + if ($useCurl) { $result = (curl -s -X PATCH "$($Endpoint)/api/rick/update" -k) | ConvertFrom-Json + $result.Result | Should -Be 'OK' } - else { - $result = Invoke-RestMethod -Uri "$($Endpoint)/api/rick/update" -Method Patch @splatter - } + + # test Invoke-RestMethod + $result = Invoke-RestMethod -Uri "$($Endpoint)/api/rick/update" -Method Patch @splatter $result.Result | Should -Be 'OK' } @@ -274,32 +279,36 @@ public bool CheckValidationResult( # compress the message using gzip $bytes = [System.Text.Encoding]::UTF8.GetBytes($message) - $ms = New-Object -TypeName System.IO.MemoryStream - $gzip = New-Object System.IO.Compression.GZipStream($ms, [IO.Compression.CompressionMode]::Compress, $true) + $ms = [System.IO.MemoryStream]::new() + $gzip = [System.IO.Compression.GZipStream]::new($ms, [IO.Compression.CompressionMode]::Compress, $true) $gzip.Write($bytes, 0, $bytes.Length) $gzip.Close() - if ($UseCurl) { + try { + # get the compressed data + $ms.Position = 0 $compressedData = $ms.ToArray() - $ms.Dispose() - # Save the compressed data to a temporary file - $tempFile = [System.IO.Path]::GetTempFileName() - [System.IO.File]::WriteAllBytes($tempFile, $compressedData) - # make the request - $result = curl -s -X POST "$Endpoint/encoding/transfer" -H 'Transfer-Encoding: gzip' -H 'Content-Type: application/json' --data-binary "@$tempFile" -k | ConvertFrom-Json - # Cleanup the temporary file - Remove-Item -Path $tempFile - } - else { + if ($useCurl) { + # Save the compressed data to a temporary file + $tempFile = [System.IO.Path]::GetTempFileName() + [System.IO.File]::WriteAllBytes($tempFile, $compressedData) + + # make the request + $result = curl -s -X POST "$Endpoint/encoding/transfer" -H 'Transfer-Encoding: gzip' -H 'Content-Type: application/json' --data-binary "@$tempFile" -k | ConvertFrom-Json + + # Cleanup the temporary file + Remove-Item -Path $tempFile + $result.Username | Should -Be 'rick' + } + # make the request - $ms.Position = 0 - $result = Invoke-RestMethod -Uri "$($Endpoint)/encoding/transfer" -Method Post -Body $ms.ToArray() -Headers @{ 'Transfer-Encoding' = 'gzip' } -ContentType 'application/json' @splatter + $result = Invoke-RestMethod -Uri "$($Endpoint)/encoding/transfer" -Method Post -Body $compressedData -Headers @{ 'Transfer-Encoding' = 'gzip' } -ContentType 'application/json' @splatter + $result.Username | Should -Be 'rick' + } + finally { $ms.Dispose() } - - $result.Username | Should -Be 'rick' - } It 'decodes encoded payload parameter - deflate' { @@ -308,32 +317,37 @@ public bool CheckValidationResult( # compress the message using deflate $bytes = [System.Text.Encoding]::UTF8.GetBytes($message) - $ms = New-Object -TypeName System.IO.MemoryStream - $gzip = New-Object System.IO.Compression.DeflateStream($ms, [IO.Compression.CompressionMode]::Compress, $true) - $gzip.Write($bytes, 0, $bytes.Length) - $gzip.Close() - if ($UseCurl) { + $ms = [System.IO.MemoryStream]::new() + $deflate = [System.IO.Compression.DeflateStream]::new($ms, [IO.Compression.CompressionMode]::Compress, $true) + $deflate.Write($bytes, 0, $bytes.Length) + $deflate.Close() + + try { + # get the compressed data + $ms.Position = 0 $compressedData = $ms.ToArray() - $ms.Dispose() - # Save the compressed data to a temporary file - $tempFile = [System.IO.Path]::GetTempFileName() - [System.IO.File]::WriteAllBytes($tempFile, $compressedData) + # test curl + if ($useCurl) { + # Save the compressed data to a temporary file + $tempFile = [System.IO.Path]::GetTempFileName() + [System.IO.File]::WriteAllBytes($tempFile, $compressedData) - # make the request - $result = curl -s -X POST "$Endpoint/encoding/transfer" -H 'Transfer-Encoding: deflate' -H 'Content-Type: application/json' --data-binary "@$tempFile" -k | ConvertFrom-Json + # make the request + $result = curl -s -X POST "$Endpoint/encoding/transfer" -H 'Transfer-Encoding: deflate' -H 'Content-Type: application/json' --data-binary "@$tempFile" -k | ConvertFrom-Json - # Cleanup the temporary file - Remove-Item -Path $tempFile + # Cleanup the temporary file + Remove-Item -Path $tempFile + $result.Username | Should -Be 'rick' + } + + # test Invoke-RestMethod + $result = Invoke-RestMethod -Uri "$($Endpoint)/encoding/transfer" -Method Post -Body $compressedData -Headers @{ 'Transfer-Encoding' = 'deflate' } -ContentType 'application/json' @splatter + $result.Username | Should -Be 'rick' } - else { - # make the request - $ms.Position = 0 - $result = Invoke-RestMethod -Uri "$($Endpoint)/encoding/transfer" -Method Post -Body $ms.ToArray() -Headers @{ 'Transfer-Encoding' = 'deflate' } -ContentType 'application/json' @splatter + finally { $ms.Dispose() } - - $result.Username | Should -Be 'rick' } It 'decodes encoded payload parameter forced to gzip' { @@ -342,36 +356,42 @@ public bool CheckValidationResult( # compress the message using gzip $bytes = [System.Text.Encoding]::UTF8.GetBytes($message) - $ms = New-Object -TypeName System.IO.MemoryStream - $gzip = New-Object System.IO.Compression.GZipStream($ms, [IO.Compression.CompressionMode]::Compress, $true) + $ms = [System.IO.MemoryStream]::new() + $gzip = [System.IO.Compression.GZipStream]::new($ms, [IO.Compression.CompressionMode]::Compress, $true) $gzip.Write($bytes, 0, $bytes.Length) $gzip.Close() - if ($UseCurl) { + try { + # get the compressed data + $ms.Position = 0 $compressedData = $ms.ToArray() - $ms.Dispose() - # Save the compressed data to a temporary file - $tempFile = [System.IO.Path]::GetTempFileName() - [System.IO.File]::WriteAllBytes($tempFile, $compressedData) - # make the request - $result = curl -s -X POST "$Endpoint/encoding/transfer-forced-type" -H 'Content-Type: application/json' --data-binary "@$tempFile" -k | ConvertFrom-Json + # test curl + if ($useCurl) { + # Save the compressed data to a temporary file + $tempFile = [System.IO.Path]::GetTempFileName() + [System.IO.File]::WriteAllBytes($tempFile, $compressedData) + + # make the request + $result = curl -s -X POST "$Endpoint/encoding/transfer-forced-type" -H 'Content-Type: application/json' --data-binary "@$tempFile" -k | ConvertFrom-Json - # Cleanup the temporary file - Remove-Item -Path $tempFile + # Cleanup the temporary file + Remove-Item -Path $tempFile + $result.Username | Should -Be 'rick' + } + + # test Invoke-RestMethod + $result = Invoke-RestMethod -Uri "$($Endpoint)/encoding/transfer-forced-type" -Method Post -Body $compressedData -ContentType 'application/json' @splatter + $result.Username | Should -Be 'rick' } - else { - # make the request - $ms.Position = 0 - $result = Invoke-RestMethod -Uri "$($Endpoint)/encoding/transfer-forced-type" -Method Post -Body $ms.ToArray() -ContentType 'application/json' @splatter + finally { $ms.Dispose() } - - $result.Username | Should -Be 'rick' } It 'works with any method' { - if ($UseCurl) { + # test curl + if ($useCurl) { $result = (curl -s -X GET "$($Endpoint)/all" -k) | ConvertFrom-Json $result.Result | Should -Be 'OK' @@ -381,20 +401,21 @@ public bool CheckValidationResult( $result = (curl -s -X PATCH "$($Endpoint)/all" -k) | ConvertFrom-Json $result.Result | Should -Be 'OK' } - else { - $result = Invoke-RestMethod -Uri "$($Endpoint)/all" -Method Get @splatter - $result.Result | Should -Be 'OK' - $result = Invoke-RestMethod -Uri "$($Endpoint)/all" -Method Put @splatter - $result.Result | Should -Be 'OK' + # test Invoke-RestMethod + $result = Invoke-RestMethod -Uri "$($Endpoint)/all" -Method Get @splatter + $result.Result | Should -Be 'OK' - $result = Invoke-RestMethod -Uri "$($Endpoint)/all" -Method Patch @splatter - $result.Result | Should -Be 'OK' - } + $result = Invoke-RestMethod -Uri "$($Endpoint)/all" -Method Put @splatter + $result.Result | Should -Be 'OK' + + $result = Invoke-RestMethod -Uri "$($Endpoint)/all" -Method Patch @splatter + $result.Result | Should -Be 'OK' } It 'route with a wild card' { - if ($UseCurl) { + # test curl + if ($useCurl) { $result = (curl -s -X GET "$($Endpoint)/api/stuff/hello" -k) | ConvertFrom-Json $result.Result | Should -Be 'OK' @@ -404,35 +425,39 @@ public bool CheckValidationResult( $result = (curl -s -X GET "$($Endpoint)/api/123/hello" -k) | ConvertFrom-Json $result.Result | Should -Be 'OK' } - else { - $result = Invoke-RestMethod -Uri "$($Endpoint)/api/stuff/hello" -Method Get @splatter - $result.Result | Should -Be 'OK' - $result = Invoke-RestMethod -Uri "$($Endpoint)/api/random/hello" -Method Get @splatter - $result.Result | Should -Be 'OK' + # test Invoke-RestMethod + $result = Invoke-RestMethod -Uri "$($Endpoint)/api/stuff/hello" -Method Get @splatter + $result.Result | Should -Be 'OK' - $result = Invoke-RestMethod -Uri "$($Endpoint)/api/123/hello" -Method Get @splatter - $result.Result | Should -Be 'OK' - } + $result = Invoke-RestMethod -Uri "$($Endpoint)/api/random/hello" -Method Get @splatter + $result.Result | Should -Be 'OK' + + $result = Invoke-RestMethod -Uri "$($Endpoint)/api/123/hello" -Method Get @splatter + $result.Result | Should -Be 'OK' } It 'route importing outer function' { - if ($UseCurl) { + # test curl + if ($useCurl) { $result = (curl -s -X GET "$($Endpoint)/imported/func/outer" -k) | ConvertFrom-Json + $result.Message | Should -Be 'Outer Hello' } - else { - $result = Invoke-RestMethod -Uri "$($Endpoint)/imported/func/outer" -Method Get @splatter - } + + # test Invoke-RestMethod + $result = Invoke-RestMethod -Uri "$($Endpoint)/imported/func/outer" -Method Get @splatter $result.Message | Should -Be 'Outer Hello' } It 'route importing outer function' { - if ($UseCurl) { + # test curl + if ($useCurl) { $result = (curl -s -X GET "$($Endpoint)/imported/func/inner" -k) | ConvertFrom-Json + $result.Message | Should -Be 'Inner Hello' } - else { - $result = Invoke-RestMethod -Uri "$($Endpoint)/imported/func/inner" -Method Get @splatter - } + + # test Invoke-RestMethod + $result = Invoke-RestMethod -Uri "$($Endpoint)/imported/func/inner" -Method Get @splatter $result.Message | Should -Be 'Inner Hello' } } \ No newline at end of file diff --git a/tests/integration/RestApi.Tests.ps1 b/tests/integration/RestApi.Tests.ps1 index 4bd8a4233..e2fd92cde 100644 --- a/tests/integration/RestApi.Tests.ps1 +++ b/tests/integration/RestApi.Tests.ps1 @@ -167,8 +167,8 @@ Describe 'REST API Requests' { # compress the message using gzip $bytes = [System.Text.Encoding]::UTF8.GetBytes($message) - $ms = New-Object -TypeName System.IO.MemoryStream - $gzip = New-Object System.IO.Compression.GZipStream($ms, [IO.Compression.CompressionMode]::Compress, $true) + $ms = [System.IO.MemoryStream]::new() + $gzip = [System.IO.Compression.GZipStream]::new($ms, [IO.Compression.CompressionMode]::Compress, $true) $gzip.Write($bytes, 0, $bytes.Length) $gzip.Close() $ms.Position = 0 @@ -184,8 +184,8 @@ Describe 'REST API Requests' { # compress the message using deflate $bytes = [System.Text.Encoding]::UTF8.GetBytes($message) - $ms = New-Object -TypeName System.IO.MemoryStream - $gzip = New-Object System.IO.Compression.DeflateStream($ms, [IO.Compression.CompressionMode]::Compress, $true) + $ms = [System.IO.MemoryStream]::new() + $gzip = [System.IO.Compression.DeflateStream]::new($ms, [IO.Compression.CompressionMode]::Compress, $true) $gzip.Write($bytes, 0, $bytes.Length) $gzip.Close() $ms.Position = 0 @@ -201,8 +201,8 @@ Describe 'REST API Requests' { # compress the message using gzip $bytes = [System.Text.Encoding]::UTF8.GetBytes($message) - $ms = New-Object -TypeName System.IO.MemoryStream - $gzip = New-Object System.IO.Compression.GZipStream($ms, [IO.Compression.CompressionMode]::Compress, $true) + $ms = [System.IO.MemoryStream]::new() + $gzip = [System.IO.Compression.GZipStream]::new($ms, [IO.Compression.CompressionMode]::Compress, $true) $gzip.Write($bytes, 0, $bytes.Length) $gzip.Close() $ms.Position = 0 diff --git a/tests/integration/Schedules.Tests.ps1 b/tests/integration/Schedules.Tests.ps1 index c4fed1f62..ab4e1f898 100644 --- a/tests/integration/Schedules.Tests.ps1 +++ b/tests/integration/Schedules.Tests.ps1 @@ -1,4 +1,5 @@ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseUsingScopeModifierInNewRunspaces', '', Justification = 'Using ArgumentList')] param() Describe 'Schedules' { @@ -18,6 +19,35 @@ Describe 'Schedules' { Close-PodeServer } + # schedule minutely using predefined cron + + Set-PodeState -Name 'test3' -Value @{eventList = @() } + + Add-PodeSchedule -Name 'TestEvents' -Cron '* * * * *' -Limit 2 -OnStart -ScriptBlock { + param($Event ) + Lock-PodeObject -ScriptBlock { + $test3 = (Get-PodeState -Name 'test3') + $test3.eventList += @{ + message = 'Hello, world!' + 'Last' = $Event.Sender.LastTriggerTime + 'Next' = $Event.Sender.NextTriggerTime + } + } + } + + + Add-PodeRoute -Method Get -Path '/eventlist' -ScriptBlock { + Lock-PodeObject -ScriptBlock { + $test3 = (Get-PodeState -Name 'test3') + if ($test3.eventList.Count -gt 1) { + Write-PodeJsonResponse -Value @{ ready = $true ; count = $test3.eventList.Count; eventList = $test3.eventList } + } + else { + Write-PodeJsonResponse -Value @{ ready = $false ; count = $test3.eventList.Count; } + } + } + } + # test1 Set-PodeState -Name 'Test1' -Value 0 Add-PodeSchedule -Name 'Test1' -Cron '* * * * *' -ScriptBlock { @@ -53,14 +83,39 @@ Describe 'Schedules' { Get-Job -Name 'Pode' | Remove-Job -Force } - - It 'schedule updates state value - full cron' { + It 'Schedule updates state value - full cron' { $result = Invoke-RestMethod -Uri "$($Endpoint)/test1" -Method Get $result.Result | Should -Be 1337 } - It 'schedule updates state value - short cron' { + It 'Schedule updates state value - short cron' { $result = Invoke-RestMethod -Uri "$($Endpoint)/test2" -Method Get $result.Result | Should -Be 314 } + + It 'Check schedule events result' { + + for ($i = 0; $i -lt 20; $i++) { + $result = Invoke-RestMethod -Uri "$($Endpoint)/eventlist" -Method Get + if ($result.ready) { + break + } + Start-Sleep -Seconds 10 + } + $result.ready | Should -BeTrue + $result.Count | Should -Be 2 + $result.eventList.GetType() | Should -Be 'System.Object[]' + $result.eventList.Count | Should -Be 2 + + + if ( $null -eq $result.eventList[0].Next ) { $index = 0 } else { $index = 1 } + $result.eventList[$index].Message | Should -Be 'Hello, world!' + $result.eventList[$index].Last | Should -not -BeNullOrEmpty + $result.eventList[$index].next | Should -BeNullOrEmpty + if ($index -eq 0) { $index = 1 }else { $index = 0 } + $result.eventList[$index].Message | Should -Be 'Hello, world!' + $result.eventList[$index].Last | Should -not -BeNullOrEmpty + $result.eventList[$index].next | Should -not -BeNullOrEmpty + } + } \ No newline at end of file diff --git a/tests/integration/specs/OpenApi-TuttiFrutti_3.0.3.json b/tests/integration/specs/OpenApi-TuttiFrutti_3.0.3.json new file mode 100644 index 000000000..4edacfd8d --- /dev/null +++ b/tests/integration/specs/OpenApi-TuttiFrutti_3.0.3.json @@ -0,0 +1,2868 @@ +{ + "openapi": "3.0.3", + "info": { + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "title": "Swagger Petstore - OpenAPI 3.0", + "version": "1.0.17", + "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about Swagger at [http://swagger.io](http://swagger.io).\r\nIn the third iteration of the pet store, we've switched to the design first approach!\r\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\r\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\r\n\r\nSome useful links:\r\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\r\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support", + "email": "apiteam@swagger.io" + } + }, + "externalDocs": { + "description": "Find out more about Swagger", + "url": "http://swagger.io" + }, + "servers": [ + { + "url": "/api/v3", + "description": "default endpoint" + } + ], + "tags": [ + { + "name": "user", + "description": "Operations about user", + "externalDocs": { + "description": "Find out more about Swagger", + "url": "http://swagger.io" + } + }, + { + "name": "store", + "description": "Access to Petstore orders", + "externalDocs": { + "description": "Find out more about Swagger", + "url": "http://swagger.io" + } + }, + { + "name": "pet", + "description": "Everything about your Pets", + "externalDocs": { + "description": "Find out more about Swagger", + "url": "http://swagger.io" + } + } + ], + "paths": { + "/pet": { + "put": { + "tags": [ + "pet" + ], + "summary": "Update an existing pet", + "description": "Update an existing pet by Id", + "operationId": "updatePet", + "requestBody": { + "$ref": "#/components/requestBodies/PetBodySchema" + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + }, + "405": { + "description": "Validation exception", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "result": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + } + } + } + } + }, + "post": { + "tags": [ + "pet" + ], + "summary": "Add a new pet to the store", + "description": "Add a new pet to the store", + "operationId": "addPet", + "requestBody": { + "$ref": "#/components/requestBodies/PetBodySchema" + }, + "security": [ + { + "Login-OAuth2": [ + "write" + ] + } + ], + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "405": { + "description": "Validation exception", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "result": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/user/{username}": { + "put": { + "tags": [ + "user" + ], + "summary": "Update user", + "description": "This can only be done by the logged in user.", + "operationId": "updateUser", + "parameters": [ + { + "description": " name that need to be updated.", + "name": "username", + "required": true, + "schema": { + "type": "string" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + }, + "required": true + }, + "responses": { + "200": { + "$ref": "#/components/responses/UserOpSuccess" + }, + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + }, + "405": { + "description": "Invalid Input" + } + } + }, + "delete": { + "tags": [ + "user" + ], + "summary": "Delete user", + "description": "This can only be done by the logged in user.", + "operationId": "deleteUser", + "parameters": [ + { + "description": "The name that needs to be deleted.", + "name": "username", + "required": true, + "schema": { + "type": "string" + }, + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + } + } + }, + "get": { + "tags": [ + "user" + ], + "summary": "Get user by user name", + "description": "Get user by user name.", + "operationId": "getUserByName", + "parameters": [ + { + "description": "The name that needs to be fetched. Use user1 for testing.", + "name": "username", + "required": true, + "schema": { + "type": "string" + }, + "in": "path" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/UserOpSuccess" + }, + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + } + } + } + }, + "/user_1/{username}": { + "put": { + "tags": [ + "user" + ], + "summary": "Update user", + "description": "This can only be done by the logged in user.", + "operationId": "updateUser_1", + "parameters": [ + { + "description": " name that need to be updated.", + "name": "username", + "required": true, + "schema": { + "type": "string" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StructPart" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/StructPart" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/StructPart" + } + } + }, + "required": true + }, + "responses": { + "200": { + "$ref": "#/components/responses/UserOpSuccess" + }, + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + }, + "405": { + "description": "Invalid Input" + } + } + } + }, + "/userLink/{username}": { + "put": { + "tags": [ + "user" + ], + "summary": "Update user", + "description": "This can only be done by the logged in user.", + "operationId": "updateUserLink", + "parameters": [ + { + "description": " name that need to be updated.", + "name": "username", + "required": true, + "schema": { + "type": "string" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + }, + "links": { + "address": { + "operationId": "getUserByName", + "parameters": { + "username": "$request.path.username" + } + } + } + }, + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + }, + "405": { + "description": "Invalid Input" + } + } + } + }, + "/userLinkByRef/{username}": { + "put": { + "tags": [ + "user" + ], + "summary": "Update user", + "description": "This can only be done by the logged in user.", + "operationId": "updateUserLinkByRef", + "parameters": [ + { + "description": " name that need to be updated.", + "name": "username", + "required": true, + "schema": { + "type": "string" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + }, + "links": { + "address2": { + "$ref": "#/components/links/address" + } + } + }, + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + }, + "405": { + "description": "Invalid Input" + } + } + } + }, + "/api/v4/paet/{petId}": { + "put": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store with form data", + "operationId": "updatepaet", + "parameters": [ + { + "examples": { + "user": { + "summary": "User Example", + "value": "http://foo.bar/examples/user-example.json" + }, + "user1": { + "summary": "User Example in XML", + "value": "http://foo.bar/examples/user-example.xml" + }, + "user2": { + "summary": "User Example in Plain text", + "value": "http://foo.bar/examples/user-example.txt" + }, + "user3": { + "summary": "User example in other forma", + "value": "http://foo.bar/examples/user-example.whatever" + } + }, + "name": "petId", + "in": "path", + "required": true, + "description": "ID of pet that needs to be updated", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Updated name of the pet" + }, + "status": { + "type": "string", + "description": "Updated status of the pet" + } + }, + "required": [ + "status" + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Pet updated.", + "content": { + "application/json": {}, + "application/xml": {} + } + }, + "405": { + "description": "Method Not Allowed", + "content": { + "application/json": {}, + "application/xml": {} + } + } + } + } + }, + "/api/v4/paet2/{petId}": { + "put": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store with form data", + "operationId": "updatepaet2", + "parameters": [ + { + "description": "ID of pet that needs to be updated", + "name": "petId", + "required": true, + "schema": { + "type": "string" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "description": "user to add to the system" + }, + "responses": { + "200": { + "description": "Pet updated.", + "content": { + "application/json": {}, + "application/xml": {} + } + }, + "405": { + "description": "Method Not Allowed", + "content": { + "application/json": {}, + "application/xml": {} + } + } + } + } + }, + "/api/v4/paet3/{petId}": { + "put": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store with form data", + "operationId": "updatepaet3", + "parameters": [ + { + "description": "ID of pet that needs to be updated", + "name": "petId", + "required": true, + "schema": { + "type": "string" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewCat" + }, + "examples": { + "cat": { + "summary": "An example of a cat", + "value": { + "color": "White", + "name": "Fluffy", + "petType": "Cat", + "gender": "male", + "breed": "Persian" + } + }, + "dog": { + "summary": "An example of a dog with a cat's name", + "value": { + "color": "Black", + "name": "Puma", + "petType": "Dog", + "gender": "Female", + "breed": "Mixed" + } + }, + "frog-example": { + "$ref": "#/components/examples/frog-example" + } + } + } + }, + "description": "user to add to the system" + }, + "responses": { + "200": { + "description": "Pet updated.", + "content": { + "application/json": {}, + "application/xml": {} + } + }, + "4XX": { + "description": "Method Not Allowed", + "content": { + "application/json": {}, + "application/xml": {} + } + } + } + } + }, + "/api/v4/paet4/{petId}": { + "put": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store with form data", + "operationId": "updatepaet4", + "parameters": [ + { + "description": "ID of pet that needs to be updated", + "name": "petId", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + }, + "required": true, + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + }, + "examples": { + "cat": { + "summary": "An example of a cat", + "value": { + "color": "White", + "name": "Fluffy", + "petType": "Cat", + "gender": "male", + "breed": "Persian" + } + }, + "dog": { + "summary": "An example of a dog with a cat's name", + "value": { + "color": "Black", + "name": "Puma", + "petType": "Dog", + "gender": "Female", + "breed": "Mixed" + } + }, + "frog-example": { + "$ref": "#/components/examples/frog-example" + } + } + } + }, + "description": "user to add to the system" + }, + "responses": { + "200": { + "description": "Pet updated.", + "content": { + "application/xml": {}, + "application/json": {} + } + }, + "405": { + "description": "Method Not Allowed", + "content": { + "application/json": {}, + "application/xml": {} + } + } + } + } + }, + "/api/v4/pat/{petId}": { + "put": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store with form data", + "operationId": "updatePasdadaetWithForm", + "parameters": [ + { + "description": "ID of pet that needs to be updated", + "name": "petId", + "required": true, + "schema": { + "type": "string" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + }, + "examples": { + "user": { + "summary": "User Example", + "externalValue": "http://foo.bar/examples/user-example.json" + } + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + }, + "examples": { + "user": { + "summary": "User Example in XML", + "externalValue": "http://foo.bar/examples/user-example.xml" + } + } + }, + "text/plain": { + "examples": { + "user": { + "summary": "User Example in Plain text", + "externalValue": "http://foo.bar/examples/user-example.txt" + } + } + }, + "\"*/*\"": { + "examples": { + "user": { + "summary": "User example in other forma", + "externalValue": "http://foo.bar/examples/user-example.whatever" + } + } + } + }, + "description": "user to add to the system" + }, + "responses": { + "200": { + "description": "Pet updated.", + "content": { + "application/json": {}, + "application/xml": {} + } + }, + "405": { + "description": "Method Not Allowed", + "content": { + "application/json": {}, + "application/xml": {} + } + } + } + } + }, + "/pet/{petId}": { + "delete": { + "tags": [ + "pet" + ], + "summary": "Deletes a pet", + "description": "Deletes a pet.", + "operationId": "deletePet", + "parameters": [ + { + "$ref": "#/components/parameters/PetIdParam" + } + ], + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } + } + }, + "get": { + "tags": [ + "pet" + ], + "summary": "Find pet by ID", + "description": "Returns a single pet.", + "operationId": "getPetById", + "parameters": [ + { + "$ref": "#/components/parameters/PetIdParam" + } + ], + "security": [ + { + "Login-OAuth2": [ + "read" + ] + } + ], + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } + } + }, + "post": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store", + "description": "Updates a pet in the store with form data", + "operationId": "updatePetWithForm", + "parameters": [ + { + "$ref": "#/components/parameters/PetIdParam" + }, + { + "description": "Name of pet that needs to be updated", + "name": "name", + "schema": { + "type": "string" + }, + "in": "query" + }, + { + "description": "Status of pet that needs to be updated", + "name": "status", + "schema": { + "type": "string" + }, + "in": "query" + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "properties": { + "file": { + "type": "array", + "items": { + "type": "string", + "format": "binary" + } + } + } + } + } + } + }, + "security": [ + { + "Login-OAuth2": [ + "write" + ] + } + ], + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Invalid ID supplied" + }, + "405": { + "description": "Invalid Input" + } + } + } + }, + "/store/order/{orderId}": { + "delete": { + "tags": [ + "store" + ], + "summary": "Delete purchase order by ID", + "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors.", + "operationId": "deleteOrder", + "parameters": [ + { + "description": " ID of the order that needs to be deleted", + "name": "orderId", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + }, + "get": { + "tags": [ + "store" + ], + "summary": "Find purchase order by ID", + "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.", + "operationId": "getOrderById", + "parameters": [ + { + "description": "ID of order that needs to be fetched", + "name": "orderId", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "in": "path" + } + ], + "servers": [ + { + "description": "ext test server", + "url": "http://ext.server.com/api/v12" + }, + { + "description": "ext test server 13", + "url": "http://ext13.server.com/api/v12" + }, + { + "description": "ext test server 14", + "url": "http://ext14.server.com/api/v12" + }, + { + "url": "/api/v3", + "description": "default endpoint" + } + ], + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + } + }, + "/pet/findByStatus": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by status", + "description": "Multiple status values can be provided with comma separated strings", + "operationId": "findPetsByStatus", + "parameters": [ + { + "description": "Status values that need to be considered for filter", + "name": "status", + "schema": { + "type": "string", + "default": "available", + "enum": [ + "available", + "pending", + "sold" + ] + }, + "in": "query" + } + ], + "security": [ + {}, + { + "Login-OAuth2": [ + "read" + ] + } + ], + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + }, + "400": { + "description": "Invalid status value" + } + } + } + }, + "/pet/findByTag": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by tags", + "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", + "operationId": "findPetsByTags", + "parameters": [ + { + "description": "Tags to filter by", + "name": "tag", + "explode": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "in": "query" + } + ], + "security": [ + { + "Login-OAuth2": [ + "read" + ] + }, + { + "api_key": [] + } + ], + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "400": { + "description": "Invalid status value" + }, + "default": { + "description": "Unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorModel" + } + } + } + } + } + } + }, + "/store/inventory": { + "get": { + "tags": [ + "store" + ], + "summary": "Returns pet inventories by status", + "description": "Returns a map of status codes to quantities", + "operationId": "getInventory", + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "none": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/user/login": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs user into the system.", + "description": "Logs user into the system.", + "operationId": "loginUser", + "parameters": [ + { + "description": "The user name for login", + "name": "username", + "schema": { + "type": "string" + }, + "in": "query" + }, + { + "description": "The password for login in clear text", + "name": "password", + "schema": { + "type": "string", + "format": "password" + }, + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful operation", + "headers": { + "X-Rate-Limit": { + "$ref": "#/components/headers/X-Rate-Limit" + }, + "X-Expires-After": { + "$ref": "#/components/headers/X-Expires-After" + } + }, + "content": { + "application/xml": { + "schema": { + "type": "string" + } + }, + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Invalid username/password supplied" + } + } + } + }, + "/user/logout": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs out current logged in user session.", + "description": "Logs out current logged in user session.", + "operationId": "logoutUser", + "responses": { + "200": { + "description": "Successful operation" + } + } + } + }, + "/peta/{id}": { + "get": { + "summary": "Find pets by ID", + "description": "Returns pets based on ID", + "operationId": "getPetsById", + "parameters": [ + { + "style": "simple", + "name": "id", + "in": "path", + "required": true, + "description": "ID of pet to use", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "pet response", + "content": { + "\"*/*\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + }, + "default": { + "description": "error payload", + "content": { + "text/html": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + } + } + } + }, + "/pet/{petId}/uploadImage2": { + "post": { + "tags": [ + "pet" + ], + "summary": "Uploads an image", + "description": "Updates a pet in the store with a new image", + "operationId": "uploadFile2", + "parameters": [ + { + "description": "ID of pet that needs to be updated", + "name": "petId", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "in": "path" + }, + { + "description": "Additional Metadata", + "name": "additionalMetadata", + "schema": { + "type": "string" + }, + "in": "query" + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "image": { + "type": "string", + "format": "binary" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "A simple string response", + "headers": { + "X-Rate-Limit-Limit": { + "description": "The number of allowed requests in the current period", + "schema": { + "type": "integer" + } + }, + "X-Rate-Limit-Remaining": { + "description": "The number of remaining requests in the current period", + "schema": { + "type": "integer" + } + }, + "X-Rate-Limit-Reset": { + "description": "The number of seconds left in the current period", + "schema": { + "type": "integer", + "maximum": 3 + } + } + }, + "content": { + "text/plain": { + "schema": { + "type": "string", + "example": "whoa!" + } + } + } + } + } + } + }, + "/pet/{petId}/uploadImageOctet": { + "post": { + "tags": [ + "pet" + ], + "summary": "Uploads an image", + "description": "Updates a pet in the store with a new image", + "operationId": "uploadFileOctet", + "parameters": [ + { + "description": "ID of pet that needs to be updated", + "name": "petId", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "in": "path" + }, + { + "description": "Additional Metadata", + "name": "additionalMetadata", + "schema": { + "type": "string" + }, + "in": "query" + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": {} + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "405": { + "description": "Invalid Input" + } + } + } + }, + "/pet/{petId}/uploadmultiImage": { + "post": { + "tags": [ + "pet" + ], + "summary": "Uploads an image", + "description": "Updates a pet in the store with a new image", + "operationId": "uploadFilemulti", + "parameters": [ + { + "description": "ID of pet that needs to be updated", + "name": "petId", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "in": "path" + }, + { + "description": "Additional Metadata", + "name": "additionalMetadata", + "schema": { + "type": "string" + }, + "in": "query" + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "orderId": { + "type": "integer" + }, + "image": { + "type": "string" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "405": { + "description": "Invalid Input" + } + } + } + }, + "/pet2/{petId}": { + "post": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store", + "description": "Updates a pet in the store with form data", + "operationId": "updatePet2WithForm", + "parameters": [ + { + "$ref": "#/components/parameters/PetIdParam" + }, + { + "description": "Name of pet that needs to be updated", + "name": "name", + "schema": { + "type": "string" + }, + "in": "query" + }, + { + "description": "Status of pet that needs to be updated", + "name": "status", + "schema": { + "type": "string" + }, + "in": "query" + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "address": { + "type": "object", + "properties": {} + } + } + } + } + } + }, + "security": [ + { + "Login-OAuth2": [ + "write" + ] + } + ], + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Invalid ID supplied" + }, + "405": { + "description": "Invalid Input" + } + } + } + }, + "/pet3/{petId}": { + "post": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store", + "description": "Updates a pet in the store with form data", + "operationId": "updatePet3WithForm", + "parameters": [ + { + "$ref": "#/components/parameters/PetIdParam" + }, + { + "description": "Name of pet that needs to be updated", + "name": "name", + "schema": { + "type": "string" + }, + "in": "query" + }, + { + "description": "Status of pet that needs to be updated", + "name": "status", + "schema": { + "type": "string" + }, + "in": "query" + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "address": { + "type": "object", + "properties": {} + }, + "children": { + "type": "array", + "items": { + "type": "string" + } + }, + "addresses": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Address" + } + } + } + } + } + } + }, + "security": [ + { + "Login-OAuth2": [ + "write" + ] + } + ], + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Invalid ID supplied" + }, + "405": { + "description": "Invalid Input" + } + } + } + }, + "/pet4/{petId}": { + "post": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store", + "description": "Updates a pet in the store with form data", + "operationId": "updatePet4WithForm", + "parameters": [ + { + "$ref": "#/components/parameters/PetIdParam" + }, + { + "description": "Name of pet that needs to be updated", + "name": "name", + "schema": { + "type": "string" + }, + "in": "query" + }, + { + "description": "Status of pet that needs to be updated", + "name": "status", + "schema": { + "type": "string" + }, + "in": "query" + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "encoding": { + "historyMetadata": { + "contentType": "application/xml; charset=utf-8" + }, + "profileImage": { + "contentType": "image/png, image/jpeg", + "headers": { + "X-Rate-Limit-Limit": { + "description": "The number of allowed requests in the current period", + "schema": { + "enum": [ + 1, + 2, + 3 + ], + "default": 3, + "type": "integer", + "maximum": 3 + } + }, + "X-Rate-Limit-Reset": { + "description": "The number of seconds left in the current period", + "schema": { + "type": "integer", + "minimum": 2 + } + } + } + } + }, + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "address": { + "type": "object", + "properties": {} + }, + "historyMetadata": { + "type": "object", + "description": "metadata in XML format", + "properties": {} + }, + "profileImage": { + "type": "string", + "format": "binary" + } + } + } + } + } + }, + "security": [ + { + "Login-OAuth2": [ + "write" + ] + } + ], + "responses": { + "200": { + "description": "A simple string response", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "405": { + "description": "Invalid Input" + } + } + } + }, + "/petcallback": { + "post": { + "tags": [ + "pet" + ], + "summary": "Add a new pet to the store", + "description": "Add a new pet to the store", + "operationId": "addPetcallback", + "requestBody": { + "$ref": "#/components/requestBodies/PetBodySchema" + }, + "callbacks": { + "test": { + "'{$request.body#/id}'": { + "post": { + "requestBody": { + "content": { + "\"*/*\"": { + "schema": { + "type": "string" + } + } + } + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + }, + "default": { + "description": "Something is wrong" + } + } + } + } + } + }, + "security": [ + { + "Login-OAuth2": [ + "write" + ] + } + ], + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "405": { + "description": "Validation exception", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "result": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/petcallbackReference": { + "post": { + "tags": [ + "pet" + ], + "summary": "Add a new pet to the store", + "description": "Add a new pet to the store", + "operationId": "petcallbackReference", + "requestBody": { + "$ref": "#/components/requestBodies/PetBodySchema" + }, + "callbacks": { + "test1": { + "$ref": "#/components/callbacks/test" + } + }, + "security": [ + { + "Login-OAuth2": [ + "write" + ] + } + ], + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "405": { + "description": "Validation exception", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "result": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/store/order": { + "post": { + "deprecated": true, + "tags": [ + "store" + ], + "summary": "Place an order for a pet", + "description": "Place a new order in the store", + "operationId": "placeOrder", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "405": { + "description": "Invalid Input" + } + } + } + }, + "/user": { + "post": { + "tags": [ + "user" + ], + "summary": "Create user.", + "description": "This can only be done by the logged in user.", + "operationId": "createUser", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + }, + "required": true + }, + "responses": { + "200": { + "$ref": "#/components/responses/UserOpSuccess" + }, + "405": { + "description": "Invalid Input", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "result": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/user/createWithList": { + "post": { + "tags": [ + "user" + ], + "summary": "Creates list of users with given input array.", + "description": "Creates list of users with given input array.", + "operationId": "createUsersWithListInput", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + }, + "required": true + }, + "responses": { + "200": { + "$ref": "#/components/responses/UserOpSuccess" + }, + "405": { + "description": "Invalid Input" + } + } + } + }, + "/close": { + "post": { + "summary": "Shutdown the server", + "responses": { + "200": { + "description": "Successful operation" + } + } + } + }, + "/stores/order/{orderId}": { + "get": { + "tags": [ + "store" + ], + "summary": "Find purchase order by ID", + "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.", + "operationId": "getOrderExternalById", + "parameters": [ + { + "description": "ID of order that needs to be fetched", + "name": "orderId", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "in": "path" + } + ], + "servers": [ + { + "description": "ext test server", + "url": "http://ext.server.com/api/v12" + }, + { + "description": "ext test server 13", + "url": "http://ext13.server.com/api/v12" + }, + { + "description": "ext test server 14", + "url": "http://ext14.server.com/api/v12" + } + ], + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + } + } + }, + "components": { + "schemas": { + "Address": { + "type": "object", + "description": "Shipping Address", + "xml": { + "name": "address" + }, + "properties": { + "street": { + "type": "string", + "example": "437 Lytton" + }, + "city": { + "type": "string", + "example": "Palo Alto" + }, + "state": { + "type": "string", + "example": "CA" + }, + "zip": { + "type": "string", + "example": "94031" + } + }, + "required": [ + "street", + "city", + "state", + "zip" + ] + }, + "Order": { + "type": "object", + "xml": { + "name": "order" + }, + "properties": { + "id": { + "type": "integer", + "readOnly": true, + "example": 10, + "format": "int64" + }, + "petId": { + "type": "integer", + "example": 198772, + "format": "int64" + }, + "quantity": { + "type": "integer", + "example": 7, + "format": "int32" + }, + "shipDate": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string", + "description": "Order Status", + "example": "approved", + "enum": [ + "placed", + "approved", + "delivered" + ] + }, + "complete": { + "type": "boolean" + }, + "Address": { + "$ref": "#/components/schemas/Address" + } + }, + "additionalProperties": { + "type": "string" + } + }, + "Category": { + "type": "object", + "xml": { + "name": "category" + }, + "properties": { + "id": { + "type": "integer", + "example": 1, + "format": "int64" + }, + "name": { + "type": "string", + "example": "Dogs" + } + } + }, + "User": { + "type": "object", + "xml": { + "name": "user" + }, + "properties": { + "id": { + "type": "integer", + "readOnly": true, + "example": 1, + "format": "int64" + }, + "username": { + "type": "string", + "example": "theUser" + }, + "firstName": { + "type": "string", + "example": "John" + }, + "lastName": { + "type": "string", + "example": "James" + }, + "email": { + "type": "string", + "example": "john@email.com", + "format": "email" + }, + "password": { + "type": "string", + "example": "12345", + "format": "password" + }, + "phone": { + "type": "string", + "example": "12345" + }, + "userStatus": { + "type": "integer", + "description": "User Status", + "example": 1, + "format": "int32" + } + }, + "required": [ + "username", + "password" + ] + }, + "aaaaa": { + "allOf": [ + { + "$ref": "#/components/schemas/Address" + }, + { + "$ref": "#/components/schemas/User" + } + ] + }, + "Tag": { + "type": "object", + "xml": { + "name": "tag" + }, + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + } + }, + "Pet": { + "type": "object", + "xml": { + "name": "pet" + }, + "properties": { + "id": { + "type": "integer", + "readOnly": true, + "example": [ + 10, + 2, + 4 + ], + "format": "int64" + }, + "name": { + "type": "string", + "example": "doggie" + }, + "category": { + "$ref": "#/components/schemas/Category" + }, + "petType": { + "type": "string", + "example": "dog" + }, + "photoUrls": { + "type": "array", + "items": { + "type": "string" + } + }, + "tags": { + "$ref": "#/components/schemas/Tag" + }, + "status": { + "type": "string", + "description": "pet status in the store", + "enum": [ + "available", + "pending", + "sold" + ] + } + }, + "required": [ + "name", + "petType" + ] + }, + "NewCat": { + "allOf": [ + { + "$ref": "#/components/schemas/Pet" + }, + { + "type": "object", + "properties": { + "huntingSkill": { + "type": "string", + "description": "The measured skill for hunting", + "enum": [ + "clueless", + "lazy", + "adventurous", + "aggressive" + ] + } + } + } + ] + }, + "XmlPrefixAndNamespace": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "xml": { + "attribute": true + }, + "format": "int32" + }, + "name": { + "type": "string", + "xml": { + "namespace": "http://example.com/schema/sample", + "prefix": "sample" + } + } + } + }, + "animals": { + "type": "array", + "items": { + "type": "string", + "xml": { + "name": "animal" + } + } + }, + "AnimalsNoAliens": { + "type": "array", + "xml": { + "name": "aliens" + }, + "items": { + "type": "string", + "xml": { + "name": "animal" + } + } + }, + "WrappedAnimals": { + "type": "array", + "xml": { + "wrapped": true + }, + "items": { + "type": "string" + } + }, + "WrappedAnimal": { + "type": "array", + "xml": { + "wrapped": true + }, + "items": { + "type": "string", + "xml": { + "name": "animal" + } + } + }, + "WrappedAliens": { + "type": "array", + "xml": { + "name": "aliens", + "wrapped": true + }, + "items": { + "type": "string", + "xml": { + "name": "animal" + } + } + }, + "WrappedAliensWithItems": { + "type": "array", + "xml": { + "name": "aliens", + "wrapped": true + }, + "items": { + "type": "string" + } + }, + "StructPart": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StructPart" + } + } + } + }, + "Pet2": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "petType": { + "type": "string" + } + }, + "discriminator": { + "propertyName": "petType" + } + }, + "Cat2": { + "allOf": [ + { + "$ref": "#/components/schemas/Pet2" + }, + { + "properties": { + "huntingSkill": { + "type": "string", + "description": "The measured skill for hunting", + "default": "lazy", + "enum": [ + "clueless", + "lazy", + "adventurous", + "aggressive" + ] + } + }, + "type": "object", + "required": [ + "huntingSkill" + ] + } + ], + "description": "A representation of a cat. Note that Cat will be used as the discriminator value." + }, + "Dog2": { + "allOf": [ + { + "$ref": "#/components/schemas/Pet2" + }, + { + "properties": { + "packSize": { + "type": "integer", + "description": "the size of the pack the dog is from", + "minimum": 0, + "format": "int32" + } + }, + "type": "object", + "required": [ + "packSize" + ] + } + ], + "description": "A representation of a dog. Note that Dog will be used as the discriminator value." + }, + "ExtendedErrorModel": { + "allOf": [ + { + "$ref": "#/components/schemas/Pet" + }, + { + "properties": { + "rootCause": { + "type": "string" + } + }, + "type": "object", + "required": [ + "rootCause" + ] + } + ] + }, + "Cat": { + "type": "object", + "description": "Type of cat", + "properties": { + "breed": { + "type": "string", + "description": "Type of Breed", + "enum": [ + "Abyssinian", + "Balinese-Javanese", + "Burmese", + "British Shorthair" + ] + } + }, + "allOf": [ + { + "$ref": "#/components/schemas/Pet" + }, + { + "properties": { + "huntingSkill": { + "type": "string", + "description": "The measured skill for hunting", + "enum": [ + "clueless", + "lazy", + "adventurous", + "aggressive" + ] + } + }, + "type": "object" + } + ] + }, + "Dog": { + "allOf": [ + { + "$ref": "#/components/schemas/Pet" + }, + { + "type": "object", + "properties": { + "breed": { + "type": "string", + "description": "Type of Breed", + "enum": [ + "Dingo", + "Husky", + "Retriever", + "Shepherd" + ] + }, + "bark": { + "type": "boolean" + } + } + } + ] + }, + "Pets": { + "oneOf": [ + { + "$ref": "#/components/schemas/Cat" + }, + { + "$ref": "#/components/schemas/Dog" + } + ], + "discriminator": { + "propertyName": "petType" + } + }, + "ApiResponse": { + "type": "object", + "xml": { + "name": "##default" + }, + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string", + "example": "doggie" + }, + "message": { + "type": "string" + } + } + }, + "ErrorModel": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "integer", + "format": "int32" + } + } + } + }, + "responses": { + "UserOpSuccess": { + "description": "Successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + } + }, + "parameters": { + "PetIdParam": { + "description": "ID of the pet", + "name": "petId", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "in": "path" + } + }, + "examples": { + "frog-example": { + "summary": "An example of a frog with a cat's name", + "value": { + "color": "Lion", + "name": "Jaguar", + "petType": "Panthera", + "gender": "Male", + "breed": "Mantella Baroni" + } + } + }, + "requestBodies": { + "PetBodySchema": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pets" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Pets" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pets" + } + } + }, + "required": true, + "description": "Pet in the store" + } + }, + "headers": { + "X-Rate-Limit": { + "schema": { + "type": "integer", + "format": "int32" + } + }, + "X-Expires-After": { + "schema": { + "type": "string", + "format": "date-time" + } + } + }, + "links": { + "address": { + "operationId": "getUserByName", + "parameters": { + "username": "$request.path.username" + } + } + }, + "callbacks": { + "test": { + "'{$request.body#/id}'": { + "post": { + "requestBody": { + "content": { + "\"*/*\"": { + "schema": { + "type": "string" + } + } + } + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + }, + "default": { + "description": "Something is wrong" + } + } + } + } + } + }, + "securitySchemes": { + "Login": { + "scheme": "basic", + "type": "http" + }, + "LoginApiKey": { + "type": "apiKey", + "in": "header", + "name": "X-API-KEY" + }, + "api_key": { + "type": "apiKey", + "in": "header", + "name": "api_key" + }, + "Jwt": { + "scheme": "bearer", + "type": "http" + }, + "Login-OAuth2": { + "type": "oauth2", + "flows": { + "authorizationCode": { + "tokenUrl": "http://example.org/api/oauth/token", + "authorizationUrl": "http://example.org/api/oauth/dialog", + "scopes": { + "read": "Grant read-only access to all your data except for the account and user info", + "write": "Grant write-only access to all your data except for the account and user info" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/integration/specs/OpenApi-TuttiFrutti_3.1.0.json b/tests/integration/specs/OpenApi-TuttiFrutti_3.1.0.json new file mode 100644 index 000000000..c3a5d1de2 --- /dev/null +++ b/tests/integration/specs/OpenApi-TuttiFrutti_3.1.0.json @@ -0,0 +1,2837 @@ +{ + "openapi": "3.1.0", + "jsonSchemaDialect": "https://spec.openapis.org/oas/3.1/dialect/base", + "info": { + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "title": "Swagger Petstore - OpenAPI 3.1", + "version": "1.0.17", + "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about Swagger at [http://swagger.io](http://swagger.io).\r\nIn the third iteration of the pet store, we've switched to the design first approach!\r\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\r\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\r\n\r\nSome useful links:\r\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\r\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support", + "email": "apiteam@swagger.io" + } + }, + "externalDocs": { + "description": "Find out more about Swagger", + "url": "http://swagger.io" + }, + "servers": [ + { + "url": "/api/v3", + "description": "default endpoint" + } + ], + "tags": [ + { + "name": "user", + "description": "Operations about user", + "externalDocs": { + "description": "Find out more about Swagger", + "url": "http://swagger.io" + } + }, + { + "name": "store", + "description": "Access to Petstore orders", + "externalDocs": { + "description": "Find out more about Swagger", + "url": "http://swagger.io" + } + }, + { + "name": "pet", + "description": "Everything about your Pets", + "externalDocs": { + "description": "Find out more about Swagger", + "url": "http://swagger.io" + } + } + ], + "paths": { + "/pet": { + "put": { + "tags": [ + "pet" + ], + "summary": "Update an existing pet", + "description": "Update an existing pet by Id", + "operationId": "updatePet", + "requestBody": { + "$ref": "#/components/requestBodies/PetBodySchema" + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + }, + "405": { + "description": "Validation exception", + "content": { + "application/json": { + "schema": { + "type": [ + "object" + ], + "properties": { + "result": { + "type": [ + "string" + ] + }, + "message": { + "type": [ + "string" + ] + } + } + } + } + } + } + } + }, + "post": { + "tags": [ + "pet" + ], + "summary": "Add a new pet to the store", + "description": "Add a new pet to the store", + "operationId": "addPet", + "requestBody": { + "$ref": "#/components/requestBodies/PetBodySchema" + }, + "security": [ + { + "Login-OAuth2": [ + "write" + ] + } + ], + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "405": { + "description": "Validation exception", + "content": { + "application/json": { + "schema": { + "type": [ + "object" + ], + "properties": { + "result": { + "type": [ + "string" + ] + }, + "message": { + "type": [ + "string" + ] + } + } + } + } + } + } + } + } + }, + "/user/{username}": { + "put": { + "tags": [ + "user" + ], + "summary": "Update user", + "description": "This can only be done by the logged in user.", + "operationId": "updateUser", + "parameters": [ + { + "description": " name that need to be updated.", + "name": "username", + "required": true, + "schema": { + "type": "string" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + }, + "required": true + }, + "responses": { + "200": { + "$ref": "#/components/responses/UserOpSuccess" + }, + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + }, + "405": { + "description": "Invalid Input" + } + } + }, + "delete": { + "tags": [ + "user" + ], + "summary": "Delete user", + "description": "This can only be done by the logged in user.", + "operationId": "deleteUser", + "parameters": [ + { + "description": "The name that needs to be deleted.", + "name": "username", + "required": true, + "schema": { + "type": "string" + }, + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + } + } + }, + "get": { + "tags": [ + "user" + ], + "summary": "Get user by user name", + "description": "Get user by user name.", + "operationId": "getUserByName", + "parameters": [ + { + "description": "The name that needs to be fetched. Use user1 for testing.", + "name": "username", + "required": true, + "schema": { + "type": "string" + }, + "in": "path" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/UserOpSuccess" + }, + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + } + } + } + }, + "/user_1/{username}": { + "put": { + "tags": [ + "user" + ], + "summary": "Update user", + "description": "This can only be done by the logged in user.", + "operationId": "updateUser_1", + "parameters": [ + { + "description": " name that need to be updated.", + "name": "username", + "required": true, + "schema": { + "type": "string" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StructPart" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/StructPart" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/StructPart" + } + } + }, + "required": true + }, + "responses": { + "200": { + "$ref": "#/components/responses/UserOpSuccess" + }, + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + }, + "405": { + "description": "Invalid Input" + } + } + } + }, + "/userLink/{username}": { + "put": { + "tags": [ + "user" + ], + "summary": "Update user", + "description": "This can only be done by the logged in user.", + "operationId": "updateUserLink", + "parameters": [ + { + "description": " name that need to be updated.", + "name": "username", + "required": true, + "schema": { + "type": "string" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + }, + "links": { + "address": { + "operationId": "getUserByName", + "parameters": { + "username": "$request.path.username" + } + } + } + }, + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + }, + "405": { + "description": "Invalid Input" + } + } + } + }, + "/userLinkByRef/{username}": { + "put": { + "tags": [ + "user" + ], + "summary": "Update user", + "description": "This can only be done by the logged in user.", + "operationId": "updateUserLinkByRef", + "parameters": [ + { + "description": " name that need to be updated.", + "name": "username", + "required": true, + "schema": { + "type": "string" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + }, + "links": { + "address2": { + "$ref": "#/components/links/address" + } + } + }, + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + }, + "405": { + "description": "Invalid Input" + } + } + } + }, + "/pet/{petId}": { + "delete": { + "tags": [ + "pet" + ], + "summary": "Deletes a pet", + "description": "Deletes a pet.", + "operationId": "deletePet", + "parameters": [ + { + "$ref": "#/components/parameters/PetIdParam" + } + ], + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } + } + }, + "get": { + "tags": [ + "pet" + ], + "summary": "Find pet by ID", + "description": "Returns a single pet.", + "operationId": "getPetById", + "parameters": [ + { + "$ref": "#/components/parameters/PetIdParam" + } + ], + "security": [ + { + "Login-OAuth2": [ + "read" + ] + } + ], + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } + } + }, + "post": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store", + "description": "Updates a pet in the store with form data", + "operationId": "updatePetWithForm", + "parameters": [ + { + "$ref": "#/components/parameters/PetIdParam" + }, + { + "description": "Name of pet that needs to be updated", + "name": "name", + "schema": { + "type": "string" + }, + "in": "query" + }, + { + "description": "Status of pet that needs to be updated", + "name": "status", + "schema": { + "type": "string" + }, + "in": "query" + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "properties": { + "file": { + "type": "array", + "items": { + "type": [ + "string" + ], + "format": "binary" + } + } + } + } + } + } + }, + "security": [ + { + "Login-OAuth2": [ + "write" + ] + } + ], + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Invalid ID supplied" + }, + "405": { + "description": "Invalid Input" + } + } + } + }, + "/store/order/{orderId}": { + "delete": { + "tags": [ + "store" + ], + "summary": "Delete purchase order by ID", + "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors.", + "operationId": "deleteOrder", + "parameters": [ + { + "description": " ID of the order that needs to be deleted", + "name": "orderId", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + }, + "get": { + "tags": [ + "store" + ], + "summary": "Find purchase order by ID", + "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.", + "operationId": "getOrderById", + "parameters": [ + { + "description": "ID of order that needs to be fetched", + "name": "orderId", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "in": "path" + } + ], + "servers": [ + { + "description": "ext test server", + "url": "http://ext.server.com/api/v12" + }, + { + "description": "ext test server 13", + "url": "http://ext13.server.com/api/v12" + }, + { + "description": "ext test server 14", + "url": "http://ext14.server.com/api/v12" + }, + { + "url": "/api/v3", + "description": "default endpoint" + } + ], + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + } + }, + "/pet/findByStatus": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by status", + "description": "Multiple status values can be provided with comma separated strings", + "operationId": "findPetsByStatus", + "parameters": [ + { + "description": "Status values that need to be considered for filter", + "name": "status", + "schema": { + "type": "string", + "default": "available", + "enum": [ + "available", + "pending", + "sold" + ] + }, + "in": "query" + } + ], + "security": [ + {}, + { + "Login-OAuth2": [ + "read" + ] + } + ], + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + }, + "400": { + "description": "Invalid status value" + } + } + } + }, + "/pet/findByTag": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by tags", + "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", + "operationId": "findPetsByTags", + "parameters": [ + { + "description": "Tags to filter by", + "name": "tag", + "explode": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "in": "query" + } + ], + "security": [ + { + "Login-OAuth2": [ + "read" + ] + }, + { + "api_key": [] + } + ], + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "400": { + "description": "Invalid status value" + }, + "default": { + "description": "Unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorModel" + } + } + } + } + } + } + }, + "/store/inventory": { + "get": { + "tags": [ + "store" + ], + "summary": "Returns pet inventories by status", + "description": "Returns a map of status codes to quantities", + "operationId": "getInventory", + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/json": { + "schema": { + "type": [ + "object" + ], + "properties": { + "none": { + "type": [ + "string" + ] + } + } + } + } + } + } + } + } + }, + "/user/login": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs user into the system.", + "description": "Logs user into the system.", + "operationId": "loginUser", + "parameters": [ + { + "description": "The user name for login", + "name": "username", + "schema": { + "type": "string" + }, + "in": "query" + }, + { + "description": "The password for login in clear text", + "name": "password", + "schema": { + "type": "string", + "format": "password" + }, + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful operation", + "headers": { + "X-Rate-Limit": { + "$ref": "#/components/headers/X-Rate-Limit" + }, + "X-Expires-After": { + "$ref": "#/components/headers/X-Expires-After" + } + }, + "content": { + "application/xml": { + "schema": { + "type": "string" + } + }, + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Invalid username/password supplied" + } + } + } + }, + "/user/logout": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs out current logged in user session.", + "description": "Logs out current logged in user session.", + "operationId": "logoutUser", + "responses": { + "200": { + "description": "Successful operation" + } + } + } + }, + "/peta/{id}": { + "get": { + "summary": "Find pets by ID", + "description": "Returns pets based on ID", + "operationId": "getPetsById", + "parameters": [ + { + "style": "simple", + "name": "id", + "in": "path", + "required": true, + "description": "ID of pet to use", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "pet response", + "content": { + "\"*/*\"": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + }, + "default": { + "description": "error payload", + "content": { + "text/html": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + } + } + } + }, + "/pet/{petId}/uploadImage2": { + "post": { + "tags": [ + "pet" + ], + "summary": "Uploads an image", + "description": "Updates a pet in the store with a new image", + "operationId": "uploadFile2", + "parameters": [ + { + "description": "ID of pet that needs to be updated", + "name": "petId", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "in": "path" + }, + { + "description": "Additional Metadata", + "name": "additionalMetadata", + "schema": { + "type": "string" + }, + "in": "query" + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": [ + "object" + ], + "properties": { + "image": { + "type": [ + "string" + ], + "format": "binary" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "A simple string response", + "headers": { + "X-Rate-Limit-Limit": { + "description": "The number of allowed requests in the current period", + "schema": { + "type": "integer" + } + }, + "X-Rate-Limit-Remaining": { + "description": "The number of remaining requests in the current period", + "schema": { + "type": "integer" + } + }, + "X-Rate-Limit-Reset": { + "description": "The number of seconds left in the current period", + "schema": { + "type": "integer", + "maximum": 3 + } + } + }, + "content": { + "text/plain": { + "schema": { + "type": [ + "string" + ], + "examples": [ + "whoa!" + ] + } + } + } + } + } + } + }, + "/pet/{petId}/uploadImageOctet": { + "post": { + "tags": [ + "pet" + ], + "summary": "Uploads an image", + "description": "Updates a pet in the store with a new image", + "operationId": "uploadFileOctet", + "parameters": [ + { + "description": "ID of pet that needs to be updated", + "name": "petId", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "in": "path" + }, + { + "description": "Additional Metadata", + "name": "additionalMetadata", + "schema": { + "type": "string" + }, + "in": "query" + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": {} + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "405": { + "description": "Invalid Input" + } + } + } + }, + "/pet/{petId}/uploadmultiImage": { + "post": { + "tags": [ + "pet" + ], + "summary": "Uploads an image", + "description": "Updates a pet in the store with a new image", + "operationId": "uploadFilemulti", + "parameters": [ + { + "description": "ID of pet that needs to be updated", + "name": "petId", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "in": "path" + }, + { + "description": "Additional Metadata", + "name": "additionalMetadata", + "schema": { + "type": "string" + }, + "in": "query" + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": [ + "object" + ], + "properties": { + "orderId": { + "type": [ + "integer" + ] + }, + "image": { + "type": [ + "string" + ] + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "405": { + "description": "Invalid Input" + } + } + } + }, + "/pet2/{petId}": { + "post": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store", + "description": "Updates a pet in the store with form data", + "operationId": "updatePet2WithForm", + "parameters": [ + { + "$ref": "#/components/parameters/PetIdParam" + }, + { + "description": "Name of pet that needs to be updated", + "name": "name", + "schema": { + "type": "string" + }, + "in": "query" + }, + { + "description": "Status of pet that needs to be updated", + "name": "status", + "schema": { + "type": "string" + }, + "in": "query" + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": [ + "object" + ], + "properties": { + "id": { + "type": [ + "string" + ], + "format": "uuid" + }, + "address": { + "type": [ + "object" + ], + "properties": {} + } + } + } + } + } + }, + "security": [ + { + "Login-OAuth2": [ + "write" + ] + } + ], + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Invalid ID supplied" + }, + "405": { + "description": "Invalid Input" + } + } + } + }, + "/pet3/{petId}": { + "post": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store", + "description": "Updates a pet in the store with form data", + "operationId": "updatePet3WithForm", + "parameters": [ + { + "$ref": "#/components/parameters/PetIdParam" + }, + { + "description": "Name of pet that needs to be updated", + "name": "name", + "schema": { + "type": "string" + }, + "in": "query" + }, + { + "description": "Status of pet that needs to be updated", + "name": "status", + "schema": { + "type": "string" + }, + "in": "query" + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": [ + "object" + ], + "properties": { + "id": { + "type": [ + "string" + ], + "format": "uuid" + }, + "address": { + "type": [ + "object" + ], + "properties": {} + }, + "children": { + "type": "array", + "items": { + "type": [ + "string" + ] + } + }, + "addresses": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Address" + } + } + } + } + } + } + }, + "security": [ + { + "Login-OAuth2": [ + "write" + ] + } + ], + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Invalid ID supplied" + }, + "405": { + "description": "Invalid Input" + } + } + } + }, + "/pet4/{petId}": { + "post": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store", + "description": "Updates a pet in the store with form data", + "operationId": "updatePet4WithForm", + "parameters": [ + { + "$ref": "#/components/parameters/PetIdParam" + }, + { + "description": "Name of pet that needs to be updated", + "name": "name", + "schema": { + "type": "string" + }, + "in": "query" + }, + { + "description": "Status of pet that needs to be updated", + "name": "status", + "schema": { + "type": "string" + }, + "in": "query" + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "encoding": { + "historyMetadata": { + "contentType": "application/xml; charset=utf-8" + }, + "profileImage": { + "contentType": "image/png, image/jpeg", + "headers": { + "X-Rate-Limit-Limit": { + "description": "The number of allowed requests in the current period", + "schema": { + "enum": [ + 1, + 2, + 3 + ], + "default": 3, + "type": "integer", + "maximum": 3 + } + }, + "X-Rate-Limit-Reset": { + "description": "The number of seconds left in the current period", + "schema": { + "type": "integer", + "minimum": 2 + } + } + } + } + }, + "schema": { + "type": [ + "object" + ], + "properties": { + "id": { + "type": [ + "string" + ], + "format": "uuid" + }, + "address": { + "type": [ + "object" + ], + "properties": {} + }, + "historyMetadata": { + "type": [ + "object" + ], + "description": "metadata in XML format", + "properties": {} + }, + "profileImage": { + "type": [ + "string" + ], + "format": "binary" + } + } + } + } + } + }, + "security": [ + { + "Login-OAuth2": [ + "write" + ] + } + ], + "responses": { + "200": { + "description": "A simple string response", + "content": { + "text/plain": { + "schema": { + "type": [ + "string" + ] + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "405": { + "description": "Invalid Input" + } + } + } + }, + "/petcallback": { + "post": { + "tags": [ + "pet" + ], + "summary": "Add a new pet to the store", + "description": "Add a new pet to the store", + "operationId": "addPetcallback", + "requestBody": { + "$ref": "#/components/requestBodies/PetBodySchema" + }, + "callbacks": { + "test": { + "'{$request.body#/id}'": { + "post": { + "requestBody": { + "content": { + "\"*/*\"": { + "schema": { + "type": [ + "string" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + }, + "default": { + "description": "Something is wrong" + } + } + } + } + } + }, + "security": [ + { + "Login-OAuth2": [ + "write" + ] + } + ], + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "405": { + "description": "Validation exception", + "content": { + "application/json": { + "schema": { + "type": [ + "object" + ], + "properties": { + "result": { + "type": [ + "string" + ] + }, + "message": { + "type": [ + "string" + ] + } + } + } + } + } + } + } + } + }, + "/petcallbackReference": { + "post": { + "tags": [ + "pet" + ], + "summary": "Add a new pet to the store", + "description": "Add a new pet to the store", + "operationId": "petcallbackReference", + "requestBody": { + "$ref": "#/components/requestBodies/PetBodySchema" + }, + "callbacks": { + "test1": { + "$ref": "#/components/callbacks/test" + } + }, + "security": [ + { + "Login-OAuth2": [ + "write" + ] + } + ], + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "405": { + "description": "Validation exception", + "content": { + "application/json": { + "schema": { + "type": [ + "object" + ], + "properties": { + "result": { + "type": [ + "string" + ] + }, + "message": { + "type": [ + "string" + ] + } + } + } + } + } + } + } + } + }, + "/store/order": { + "post": { + "deprecated": true, + "tags": [ + "store" + ], + "summary": "Place an order for a pet", + "description": "Place a new order in the store", + "operationId": "placeOrder", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "405": { + "description": "Invalid Input" + } + } + } + }, + "/user": { + "post": { + "tags": [ + "user" + ], + "summary": "Create user.", + "description": "This can only be done by the logged in user.", + "operationId": "createUser", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + }, + "required": true + }, + "responses": { + "200": { + "$ref": "#/components/responses/UserOpSuccess" + }, + "405": { + "description": "Invalid Input", + "content": { + "application/json": { + "schema": { + "type": [ + "object" + ], + "properties": { + "result": { + "type": [ + "string" + ] + }, + "message": { + "type": [ + "string" + ] + } + } + } + } + } + } + } + } + }, + "/user/createWithList": { + "post": { + "tags": [ + "user" + ], + "summary": "Creates list of users with given input array.", + "description": "Creates list of users with given input array.", + "operationId": "createUsersWithListInput", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + }, + "required": true + }, + "responses": { + "200": { + "$ref": "#/components/responses/UserOpSuccess" + }, + "405": { + "description": "Invalid Input" + } + } + } + }, + "/close": { + "post": { + "summary": "Shutdown the server", + "responses": { + "200": { + "description": "Successful operation" + } + } + } + }, + "/stores/order/{orderId}": { + "get": { + "tags": [ + "store" + ], + "summary": "Find purchase order by ID", + "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.", + "operationId": "getOrderExternalById", + "parameters": [ + { + "description": "ID of order that needs to be fetched", + "name": "orderId", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "in": "path" + } + ], + "servers": [ + { + "description": "ext test server", + "url": "http://ext.server.com/api/v12" + }, + { + "description": "ext test server 13", + "url": "http://ext13.server.com/api/v12" + }, + { + "description": "ext test server 14", + "url": "http://ext14.server.com/api/v12" + } + ], + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + } + } + }, + "webhooks": { + "newPet": { + "post": { + "description": "Information about a new pet in the system", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pets" + } + } + } + }, + "responses": { + "200": { + "description": "Return a 200 status to indicate that the data was received successfully" + } + } + } + } + }, + "components": { + "schemas": { + "Address": { + "type": [ + "object" + ], + "description": "Shipping Address", + "xml": { + "name": "address" + }, + "properties": { + "street": { + "type": [ + "string" + ], + "examples": [ + "437 Lytton" + ] + }, + "city": { + "type": [ + "string" + ], + "examples": [ + "Palo Alto" + ] + }, + "state": { + "type": [ + "string" + ], + "examples": [ + "CA" + ] + }, + "zip": { + "type": [ + "string" + ], + "examples": [ + "94031" + ] + } + }, + "required": [ + "street", + "city", + "state", + "zip" + ] + }, + "Order": { + "type": [ + "object" + ], + "xml": { + "name": "order" + }, + "properties": { + "id": { + "type": [ + "integer" + ], + "readOnly": true, + "examples": [ + 10 + ], + "format": "int64" + }, + "petId": { + "type": [ + "integer" + ], + "examples": [ + 198772 + ], + "format": "int64" + }, + "quantity": { + "type": [ + "integer" + ], + "examples": [ + 7 + ], + "format": "int32" + }, + "shipDate": { + "type": [ + "string" + ], + "format": "date-time" + }, + "status": { + "type": [ + "string" + ], + "description": "Order Status", + "examples": [ + "approved" + ], + "enum": [ + "placed", + "approved", + "delivered" + ] + }, + "complete": { + "type": [ + "boolean" + ] + }, + "Address": { + "$ref": "#/components/schemas/Address" + } + }, + "additionalProperties": { + "type": [ + "string" + ] + } + }, + "Category": { + "type": [ + "object" + ], + "xml": { + "name": "category" + }, + "properties": { + "id": { + "type": [ + "integer" + ], + "examples": [ + 1 + ], + "format": "int64" + }, + "name": { + "type": [ + "string" + ], + "examples": [ + "Dogs" + ] + } + } + }, + "User": { + "type": [ + "object" + ], + "xml": { + "name": "user" + }, + "properties": { + "id": { + "type": [ + "integer" + ], + "readOnly": true, + "examples": [ + 1 + ], + "format": "int64" + }, + "username": { + "type": [ + "string" + ], + "examples": [ + "theUser" + ] + }, + "firstName": { + "type": [ + "string" + ], + "examples": [ + "John" + ] + }, + "lastName": { + "type": [ + "string" + ], + "examples": [ + "James" + ] + }, + "email": { + "type": [ + "string" + ], + "examples": [ + "john@email.com" + ], + "format": "email" + }, + "password": { + "type": [ + "string" + ], + "examples": [ + "12345" + ], + "format": "password" + }, + "phone": { + "type": [ + "string" + ], + "examples": [ + "12345" + ] + }, + "userStatus": { + "type": [ + "integer" + ], + "description": "User Status", + "examples": [ + 1 + ], + "format": "int32" + } + }, + "required": [ + "username", + "password" + ] + }, + "aaaaa": { + "allOf": [ + { + "$ref": "#/components/schemas/Address" + }, + { + "$ref": "#/components/schemas/User" + } + ] + }, + "Tag": { + "type": [ + "object" + ], + "xml": { + "name": "tag" + }, + "properties": { + "id": { + "type": [ + "integer" + ], + "format": "int64" + }, + "name": { + "type": [ + "string" + ] + } + } + }, + "Pet": { + "type": [ + "object" + ], + "xml": { + "name": "pet" + }, + "properties": { + "id": { + "type": [ + "integer" + ], + "readOnly": true, + "examples": [ + 10, + 2, + 4 + ], + "format": "int64" + }, + "name": { + "type": [ + "string" + ], + "examples": [ + "doggie" + ] + }, + "category": { + "$ref": "#/components/schemas/Category" + }, + "petType": { + "type": [ + "string" + ], + "examples": [ + "dog" + ] + }, + "photoUrls": { + "type": "array", + "items": { + "type": [ + "string" + ] + } + }, + "tags": { + "$ref": "#/components/schemas/Tag" + }, + "status": { + "type": [ + "string" + ], + "description": "pet status in the store", + "enum": [ + "available", + "pending", + "sold" + ] + } + }, + "required": [ + "name", + "petType" + ] + }, + "NewCat": { + "allOf": [ + { + "$ref": "#/components/schemas/Pet" + }, + { + "type": [ + "object" + ], + "properties": { + "huntingSkill": { + "type": [ + "string" + ], + "description": "The measured skill for hunting", + "enum": [ + "clueless", + "lazy", + "adventurous", + "aggressive" + ] + } + } + } + ] + }, + "XmlPrefixAndNamespace": { + "type": [ + "object" + ], + "properties": { + "id": { + "type": [ + "integer" + ], + "xml": { + "attribute": true + }, + "format": "int32" + }, + "name": { + "type": [ + "string" + ], + "xml": { + "namespace": "http://example.com/schema/sample", + "prefix": "sample" + } + } + } + }, + "animals": { + "type": "array", + "items": { + "type": [ + "string" + ], + "xml": { + "name": "animal" + } + } + }, + "AnimalsNoAliens": { + "type": "array", + "xml": { + "name": "aliens" + }, + "items": { + "type": [ + "string" + ], + "xml": { + "name": "animal" + } + } + }, + "WrappedAnimals": { + "type": "array", + "xml": { + "wrapped": true + }, + "items": { + "type": [ + "string" + ] + } + }, + "WrappedAnimal": { + "type": "array", + "xml": { + "wrapped": true + }, + "items": { + "type": [ + "string" + ], + "xml": { + "name": "animal" + } + } + }, + "WrappedAliens": { + "type": "array", + "xml": { + "name": "aliens", + "wrapped": true + }, + "items": { + "type": [ + "string" + ], + "xml": { + "name": "animal" + } + } + }, + "WrappedAliensWithItems": { + "type": "array", + "xml": { + "name": "aliens", + "wrapped": true + }, + "items": { + "type": [ + "string" + ] + } + }, + "StructPart": { + "type": [ + "object" + ], + "properties": { + "name": { + "type": [ + "string" + ] + }, + "type": { + "type": [ + "string" + ] + }, + "children": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StructPart" + } + } + } + }, + "Pet2": { + "type": [ + "object" + ], + "properties": { + "name": { + "type": [ + "string" + ] + }, + "petType": { + "type": [ + "string" + ] + } + }, + "discriminator": { + "propertyName": "petType" + } + }, + "Cat2": { + "allOf": [ + { + "$ref": "#/components/schemas/Pet2" + }, + { + "properties": { + "huntingSkill": { + "type": [ + "string" + ], + "description": "The measured skill for hunting", + "default": "lazy", + "enum": [ + "clueless", + "lazy", + "adventurous", + "aggressive" + ] + } + }, + "type": "object", + "required": [ + "huntingSkill" + ] + } + ], + "description": "A representation of a cat. Note that Cat will be used as the discriminator value." + }, + "Dog2": { + "allOf": [ + { + "$ref": "#/components/schemas/Pet2" + }, + { + "properties": { + "packSize": { + "type": [ + "integer" + ], + "description": "the size of the pack the dog is from", + "minimum": 0, + "format": "int32" + } + }, + "type": "object", + "required": [ + "packSize" + ] + } + ], + "description": "A representation of a dog. Note that Dog will be used as the discriminator value." + }, + "ExtendedErrorModel": { + "allOf": [ + { + "$ref": "#/components/schemas/Pet" + }, + { + "properties": { + "rootCause": { + "type": [ + "string" + ] + } + }, + "type": "object", + "required": [ + "rootCause" + ] + } + ] + }, + "Cat": { + "type": [ + "object" + ], + "description": "Type of cat", + "properties": { + "breed": { + "type": [ + "string" + ], + "description": "Type of Breed", + "enum": [ + "Abyssinian", + "Balinese-Javanese", + "Burmese", + "British Shorthair" + ] + } + }, + "allOf": [ + { + "$ref": "#/components/schemas/Pet" + }, + { + "properties": { + "huntingSkill": { + "type": [ + "string" + ], + "description": "The measured skill for hunting", + "enum": [ + "clueless", + "lazy", + "adventurous", + "aggressive" + ] + } + }, + "type": "object" + } + ] + }, + "Dog": { + "allOf": [ + { + "$ref": "#/components/schemas/Pet" + }, + { + "type": [ + "object" + ], + "properties": { + "breed": { + "type": [ + "string" + ], + "description": "Type of Breed", + "enum": [ + "Dingo", + "Husky", + "Retriever", + "Shepherd" + ] + }, + "bark": { + "type": [ + "boolean" + ] + } + } + } + ] + }, + "Pets": { + "oneOf": [ + { + "$ref": "#/components/schemas/Cat" + }, + { + "$ref": "#/components/schemas/Dog" + } + ], + "discriminator": { + "propertyName": "petType" + } + }, + "ApiResponse": { + "type": [ + "object" + ], + "xml": { + "name": "##default" + }, + "properties": { + "code": { + "type": [ + "integer" + ], + "format": "int32" + }, + "type": { + "type": [ + "string" + ], + "examples": [ + "doggie" + ] + }, + "message": { + "type": [ + "string" + ] + } + } + }, + "ErrorModel": { + "type": [ + "object" + ], + "properties": { + "message": { + "type": [ + "string" + ] + }, + "code": { + "type": [ + "integer" + ], + "format": "int32" + } + } + } + }, + "responses": { + "UserOpSuccess": { + "description": "Successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + } + }, + "parameters": { + "PetIdParam": { + "description": "ID of the pet", + "name": "petId", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "in": "path" + } + }, + "requestBodies": { + "PetBodySchema": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pets" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Pets" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pets" + } + } + }, + "required": true, + "description": "Pet in the store" + } + }, + "headers": { + "X-Rate-Limit": { + "schema": { + "type": [ + "integer" + ], + "format": "int32" + } + }, + "X-Expires-After": { + "schema": { + "type": [ + "string" + ], + "format": "date-time" + } + } + }, + "links": { + "address": { + "operationId": "getUserByName", + "parameters": { + "username": "$request.path.username" + } + } + }, + "callbacks": { + "test": { + "'{$request.body#/id}'": { + "post": { + "requestBody": { + "content": { + "\"*/*\"": { + "schema": { + "type": [ + "string" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + }, + "default": { + "description": "Something is wrong" + } + } + } + } + } + }, + "pathItems": { + "GetPetByIdWithRef": { + "get": { + "tags": [ + "pet" + ], + "summary": "Find pet by ID", + "description": "Returns a single pet.", + "operationId": "getPetByIdWithRef", + "parameters": [ + { + "description": "ID of pet to return", + "name": "petId", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + }, + "415": { + "description": "Unsupported Media Type" + } + } + } + } + }, + "securitySchemes": { + "Login": { + "scheme": "basic", + "type": "http" + }, + "LoginApiKey": { + "type": "apiKey", + "in": "header", + "name": "X-API-KEY" + }, + "api_key": { + "type": "apiKey", + "in": "header", + "name": "api_key" + }, + "Jwt": { + "scheme": "bearer", + "type": "http" + }, + "Login-OAuth2": { + "type": "oauth2", + "flows": { + "authorizationCode": { + "tokenUrl": "http://example.org/api/oauth/token", + "authorizationUrl": "http://example.org/api/oauth/dialog", + "scopes": { + "read": "Grant read-only access to all your data except for the account and user info", + "write": "Grant write-only access to all your data except for the account and user info" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/unit/Authentication.Tests.ps1 b/tests/unit/Authentication.Tests.ps1 index d5f529726..38190f523 100644 --- a/tests/unit/Authentication.Tests.ps1 +++ b/tests/unit/Authentication.Tests.ps1 @@ -4,6 +4,7 @@ BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' } $now = [datetime]::UtcNow @@ -138,25 +139,25 @@ Describe 'Test-PodeJwt' { It 'Throws exception - the JWT has expired' { # "exp" (Expiration Time) Claim - { Test-PodeJwt @{exp = 1 } } | Should -Throw -ExceptionType ([System.Exception]) -ExpectedMessage 'The JWT has expired' + { Test-PodeJwt @{exp = 1 } } | Should -Throw -ExceptionType ([System.Exception]) -ExpectedMessage $PodeLocale.jwtExpiredExceptionMessage } It 'Throws exception - the JWT is not yet valid for use' { # "nbf" (Not Before) Claim - { Test-PodeJwt @{nbf = 99999999999 } } | Should -Throw -ExceptionType ([System.Exception]) -ExpectedMessage 'The JWT is not yet valid for use' + { Test-PodeJwt @{nbf = 99999999999 } } | Should -Throw -ExceptionType ([System.Exception]) -ExpectedMessage $PodeLocale.jwtNotYetValidExceptionMessage } } -Describe "Expand-PodeAuthMerge Tests" { +Describe 'Expand-PodeAuthMerge Tests' { BeforeAll { # Mock the $PodeContext variable $PodeContext = @{ Server = @{ Authentications = @{ Methods = @{ - BasicAuth = @{ Name = 'BasicAuth'; merged = $false } - ApiKeyAuth = @{ Name = 'ApiKeyAuth'; merged = $false } + BasicAuth = @{ Name = 'BasicAuth'; merged = $false } + ApiKeyAuth = @{ Name = 'ApiKeyAuth'; merged = $false } CustomMergedAuth = @{ Name = 'CustomMergedAuth'; merged = $true; Authentications = @('BasicAuth', 'ApiKeyAuth') } } } @@ -164,27 +165,27 @@ Describe "Expand-PodeAuthMerge Tests" { } } - It "Expands discrete authentication methods correctly" { + It 'Expands discrete authentication methods correctly' { $expandedAuthNames = Expand-PodeAuthMerge -Names @('BasicAuth', 'ApiKeyAuth') $expandedAuthNames | Should -Contain 'BasicAuth' $expandedAuthNames | Should -Contain 'ApiKeyAuth' $expandedAuthNames.Count | Should -Be 2 } - It "Expands merged authentication methods into individual components" { + It 'Expands merged authentication methods into individual components' { $expandedAuthNames = Expand-PodeAuthMerge -Names @('CustomMergedAuth') $expandedAuthNames | Should -Contain 'BasicAuth' $expandedAuthNames | Should -Contain 'ApiKeyAuth' $expandedAuthNames.Count | Should -Be 2 } - It "Handles anonymous access special case" { + It 'Handles anonymous access special case' { $expandedAuthNames = Expand-PodeAuthMerge -Names @('%_allowanon_%') $expandedAuthNames | Should -Contain '%_allowanon_%' $expandedAuthNames.Count | Should -Be 1 } - It "Handles empty and invalid inputs" { + It 'Handles empty and invalid inputs' { { Expand-PodeAuthMerge -Names @() } | Should -Throw { Expand-PodeAuthMerge -Names @('NonExistentAuth') } | Should -Throw } diff --git a/tests/unit/Context.Tests.ps1 b/tests/unit/Context.Tests.ps1 index dc89568b1..6006e6428 100644 --- a/tests/unit/Context.Tests.ps1 +++ b/tests/unit/Context.Tests.ps1 @@ -4,6 +4,7 @@ BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' $PodeContext = @{ 'Server' = $null; } } @@ -21,7 +22,7 @@ Describe 'Get-PodeConfig' { Describe 'Add-PodeEndpoint' { Context 'Invalid parameters supplied' { It 'Throw invalid type error for no protocol' { - { Add-PodeEndpoint -Address '127.0.0.1' -Protocol 'MOO' } | Should -Throw -ExpectedMessage "Cannot validate argument on parameter 'Protocol'*" + { Add-PodeEndpoint -Address '127.0.0.1' -Protocol 'MOO' } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Add-PodeEndpoint' } } @@ -225,7 +226,7 @@ Describe 'Add-PodeEndpoint' { It 'Throws error for an invalid IPv4' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - { Add-PodeEndpoint -Address '256.0.0.1' -Protocol 'HTTP' } | Should -Throw -ExpectedMessage '*Invalid IP Address*' + { Add-PodeEndpoint -Address '256.0.0.1' -Protocol 'HTTP' } | Should -Throw -ErrorId 'FormatException,Get-PodeIPAddress' $PodeContext.Server.Types | Should -Be $null $PodeContext.Server.Endpoints.Count | Should -Be 0 @@ -233,7 +234,7 @@ Describe 'Add-PodeEndpoint' { It 'Throws error for an invalid IPv4 address with port' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - { Add-PodeEndpoint -Address '256.0.0.1' -Port 80 -Protocol 'HTTP' } | Should -Throw -ExpectedMessage '*Invalid IP Address*' + { Add-PodeEndpoint -Address '256.0.0.1' -Port 80 -Protocol 'HTTP' } | Should -Throw -ErrorId 'FormatException,Get-PodeIPAddress' $PodeContext.Server.Types | Should -Be $null $PodeContext.Server.Endpoints.Count | Should -Be 0 @@ -326,7 +327,7 @@ Describe 'Add-PodeEndpoint' { It 'Throws error when adding two endpoints with the same name' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'HTTP' -Name 'Example' - { Add-PodeEndpoint -Address 'pode.foo.com' -Port 80 -Protocol 'HTTP' -Name 'Example' } | Should -Throw -ExpectedMessage '*already been defined*' + { Add-PodeEndpoint -Address 'pode.foo.com' -Port 80 -Protocol 'HTTP' -Name 'Example' } | Should -Throw -ExpectedMessage ($PodeLocale.endpointAlreadyDefinedExceptionMessage -f 'Example') #'*already been defined*' } It 'Add two endpoints to listen on, one of SMTP and one of SMTPS' { @@ -372,7 +373,7 @@ Describe 'Add-PodeEndpoint' { It 'Throws an error for not running as admin' { Mock Test-PodeIsAdminUser { return $false } $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - { Add-PodeEndpoint -Address '127.0.0.2' -Protocol 'HTTP' } | Should -Throw -ExpectedMessage '*Must be running with admin*' + { Add-PodeEndpoint -Address '127.0.0.2' -Protocol 'HTTP' } | Should -Throw -ExpectedMessage $PodeLocale.mustBeRunningWithAdminPrivilegesExceptionMessage #'*Must be running with admin*' } } } @@ -501,19 +502,19 @@ Describe 'Get-PodeEndpoint' { Describe 'Import-PodeModule' { Context 'Invalid parameters supplied' { It 'Throw null path parameter error' { - { Import-PodeModule -Path $null } | Should -Throw -ExpectedMessage '*it is an empty string*' + { Import-PodeModule -Path $null } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorEmptyStringNotAllowed,Import-PodeModule' } It 'Throw empty path parameter error' { - { Import-PodeModule -Path ([string]::Empty) } | Should -Throw -ExpectedMessage '*it is an empty string*' + { Import-PodeModule -Path ([string]::Empty) } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorEmptyStringNotAllowed,Import-PodeModule' } It 'Throw null name parameter error' { - { Import-PodeModule -Name $null } | Should -Throw -ExpectedMessage '*it is an empty string*' + { Import-PodeModule -Name $null } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorEmptyStringNotAllowed,Import-PodeModule' } It 'Throw empty name parameter error' { - { Import-PodeModule -Name ([string]::Empty) } | Should -Throw -ExpectedMessage '*it is an empty string*' + { Import-PodeModule -Name ([string]::Empty) } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorEmptyStringNotAllowed,Import-PodeModule' } } diff --git a/tests/unit/Cookies.Tests.ps1 b/tests/unit/Cookies.Tests.ps1 index f9a992fd2..294901e9f 100644 --- a/tests/unit/Cookies.Tests.ps1 +++ b/tests/unit/Cookies.Tests.ps1 @@ -4,6 +4,7 @@ BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' } Describe 'Test-PodeCookie' { It 'Returns true' { @@ -49,7 +50,7 @@ Describe 'Test-PodeCookieSigned' { } } - { Test-PodeCookieSigned -Name 'test' } | Should -Throw -ExpectedMessage '*argument is null*' + { Test-PodeCookieSigned -Name 'test' } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Test-PodeCookieSigned' Assert-MockCalled Invoke-PodeValueUnsign -Times 0 -Scope It } @@ -478,19 +479,19 @@ Describe 'Remove-PodeCookie' { Describe 'Invoke-PodeValueSign' { Context 'Invalid parameters supplied' { It 'Throws null value error' { - { Invoke-PodeValueSign -Value $null -Secret 'key' } | Should -Throw -ExpectedMessage '*argument is null or empty*' + { Invoke-PodeValueSign -Value $null -Secret 'key' } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Invoke-PodeValueSign' } It 'Throws empty value error' { - { Invoke-PodeValueSign -Value '' -Secret 'key' } | Should -Throw -ExpectedMessage '*argument is null or empty*' + { Invoke-PodeValueSign -Value '' -Secret 'key' } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Invoke-PodeValueSign' } It 'Throws null secret error' { - { Invoke-PodeValueSign -Value 'value' -Secret $null } | Should -Throw -ExpectedMessage '*argument is null or empty*' + { Invoke-PodeValueSign -Value 'value' -Secret $null } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Invoke-PodeValueSign' } It 'Throws empty secret error' { - { Invoke-PodeValueSign -Value 'value' -Secret '' } | Should -Throw -ExpectedMessage '*argument is null or empty*' + { Invoke-PodeValueSign -Value 'value' -Secret '' } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Invoke-PodeValueSign' } } @@ -504,19 +505,19 @@ Describe 'Invoke-PodeValueSign' { Describe 'Invoke-PodeValueUnsign' { Context 'Invalid parameters supplied' { It 'Throws null value error' { - { Invoke-PodeValueUnsign -Value $null -Secret 'key' } | Should -Throw -ExpectedMessage '*argument is null or empty*' + { Invoke-PodeValueUnsign -Value $null -Secret 'key' } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Invoke-PodeValueUnsign' } It 'Throws empty value error' { - { Invoke-PodeValueUnsign -Value '' -Secret 'key' } | Should -Throw -ExpectedMessage '*argument is null or empty*' + { Invoke-PodeValueUnsign -Value '' -Secret 'key' } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Invoke-PodeValueUnsign' } It 'Throws null secret error' { - { Invoke-PodeValueUnsign -Value 'value' -Secret $null } | Should -Throw -ExpectedMessage '*argument is null or empty*' + { Invoke-PodeValueUnsign -Value 'value' -Secret $null } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Invoke-PodeValueUnsign' } It 'Throws empty secret error' { - { Invoke-PodeValueUnsign -Value 'value' -Secret '' } | Should -Throw -ExpectedMessage '*argument is null or empty*' + { Invoke-PodeValueUnsign -Value 'value' -Secret '' } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Invoke-PodeValueUnsign' } } diff --git a/tests/unit/CronParser.Tests.ps1 b/tests/unit/CronParser.Tests.ps1 index b5d7454e4..5692d6324 100644 --- a/tests/unit/CronParser.Tests.ps1 +++ b/tests/unit/CronParser.Tests.ps1 @@ -4,11 +4,12 @@ BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' } -Describe 'Get-PodeCronFields' { +Describe 'Get-PodeCronField' { It 'Returns valid cron fields' { - Get-PodeCronFields | Should -Be @( + Get-PodeCronField | Should -Be @( 'Minute', 'Hour', 'DayOfMonth', @@ -18,9 +19,9 @@ Describe 'Get-PodeCronFields' { } } -Describe 'Get-PodeCronFieldConstraints' { +Describe 'Get-PodeCronFieldConstraint' { It 'Returns valid cron field constraints' { - $constraints = Get-PodeCronFieldConstraints + $constraints = Get-PodeCronFieldConstraint $constraints | Should -Not -Be $null $constraints.MinMax | Should -Be @( @@ -64,9 +65,9 @@ Describe 'Get-PodeCronPredefined' { } } -Describe 'Get-PodeCronFieldAliases' { +Describe 'Get-PodeCronFieldAlias' { It 'Returns valid aliases' { - $aliases = Get-PodeCronFieldAliases + $aliases = Get-PodeCronFieldAlias $aliases | Should -Not -Be $null $aliases.Month.Jan | Should -Be 1 @@ -95,41 +96,41 @@ Describe 'Get-PodeCronFieldAliases' { Describe 'ConvertFrom-PodeCronExpression' { Context 'Invalid parameters supplied' { It 'Throw null expression parameter error' { - { ConvertFrom-PodeCronExpression -Expression $null } | Should -Throw -ExpectedMessage '*The argument is null or empty*' + { ConvertFrom-PodeCronExpression -Expression $null } | Should -Throw -ErrorId 'ParameterArgumentValidationError,ConvertFrom-PodeCronExpression' } It 'Throw empty expression parameter error' { - { ConvertFrom-PodeCronExpression -Expression ([string]::Empty) } | Should -Throw -ExpectedMessage '*The argument is null or empty*' + { ConvertFrom-PodeCronExpression -Expression ([string]::Empty) } | Should -Throw -ErrorId 'ParameterArgumentValidationError,ConvertFrom-PodeCronExpression' } } Context 'Valid schedule parameters' { It 'Throws error for too few number of cron atoms' { - { ConvertFrom-PodeCronExpression -Expression '* * *' } | Should -Throw -ExpectedMessage '*Cron expression should only consist of 5 parts*' + { ConvertFrom-PodeCronExpression -Expression '* * *' } | Should -Throw -ExpectedMessage ($PodeLocale.cronExpressionInvalidExceptionMessage -f '* * *') #'*Cron expression should only consist of 5 parts*' } It 'Throws error for too many number of cron atoms' { - { ConvertFrom-PodeCronExpression -Expression '* * * * * *' } | Should -Throw -ExpectedMessage '*Cron expression should only consist of 5 parts*' + { ConvertFrom-PodeCronExpression -Expression '* * * * * *' } | Should -Throw -ExpectedMessage ($PodeLocale.cronExpressionInvalidExceptionMessage -f '* * * * * *') #'*Cron expression should only consist of 5 parts*' } It 'Throws error for range atom with min>max' { - { ConvertFrom-PodeCronExpression -Expression '* * 20-15 * *' } | Should -Throw -ExpectedMessage '*should not be greater than the max value*' + { ConvertFrom-PodeCronExpression -Expression '* * 20-15 * *' } | Should -Throw -ExpectedMessage ($PodeLocale.minValueGreaterThanMaxExceptionMessage -f 'DayOfMonth') #'*should not be greater than the max value*' } It 'Throws error for range atom with invalid min' { - { ConvertFrom-PodeCronExpression -Expression '* * 0-5 * *' } | Should -Throw -ExpectedMessage '*is invalid, should be greater than/equal to*' + { ConvertFrom-PodeCronExpression -Expression '* * 0-5 * *' } | Should -Throw -ExpectedMessage ($PodeLocale.minValueInvalidExceptionMessage -f 0,'DayOfMonth',1) # '*is invalid, should be greater than/equal to*' } It 'Throws error for range atom with invalid max' { - { ConvertFrom-PodeCronExpression -Expression '* * 1-32 * *' } | Should -Throw -ExpectedMessage '*is invalid, should be less than/equal to*' + { ConvertFrom-PodeCronExpression -Expression '* * 1-32 * *' } | Should -Throw -ExpectedMessage ($PodeLocale.maxValueInvalidExceptionMessage -f 32,'DayOfMonth',31) #'*is invalid, should be less than/equal to*' } It 'Throws error for atom with invalid min' { - { ConvertFrom-PodeCronExpression -Expression '* * 0 * *' } | Should -Throw -ExpectedMessage '*invalid, should be between*' + { ConvertFrom-PodeCronExpression -Expression '* * 0 * *' } | Should -Throw -ExpectedMessage ($PodeLocale.valueOutOfRangeExceptionMessage -f '','DayOfMonth',1,31) # '*invalid, should be between*' } It 'Throws error for atom with invalid max' { - { ConvertFrom-PodeCronExpression -Expression '* * 32 * *' } | Should -Throw -ExpectedMessage '*invalid, should be between*' + { ConvertFrom-PodeCronExpression -Expression '* * 32 * *' } | Should -Throw -ExpectedMessage ($PodeLocale.valueOutOfRangeExceptionMessage -f '','DayOfMonth',1,31)#'*invalid, should be between*' } @@ -501,50 +502,50 @@ Describe 'Get-PodeCronNextTrigger' { } It 'Returns the next minute' { $exp = '* * * * *' - $cron = ConvertFrom-PodeCronExpressions -Expressions $exp + $cron = ConvertFrom-PodeCronExpression -Expression $exp Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should -Be ([datetime]::new(2020, 1, 1, 0, 1, 0)) } It 'Returns the next hour' { $exp = '0 * * * *' - $cron = ConvertFrom-PodeCronExpressions -Expressions $exp + $cron = ConvertFrom-PodeCronExpression -Expression $exp Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should -Be ([datetime]::new(2020, 1, 1, 1, 0, 0)) } It 'Returns the next day' { $exp = '0 0 * * *' - $cron = ConvertFrom-PodeCronExpressions -Expressions $exp + $cron = ConvertFrom-PodeCronExpression -Expression $exp Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should -Be ([datetime]::new(2020, 1, 2, 0, 0, 0)) } It 'Returns the next month' { $exp = '0 0 1 * *' - $cron = ConvertFrom-PodeCronExpressions -Expressions $exp + $cron = ConvertFrom-PodeCronExpression -Expression $exp Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should -Be ([datetime]::new(2020, 2, 1, 0, 0, 0)) } It 'Returns the next year' { $exp = '0 0 1 1 *' - $cron = ConvertFrom-PodeCronExpressions -Expressions $exp + $cron = ConvertFrom-PodeCronExpression -Expression $exp Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should -Be ([datetime]::new(2021, 1, 1, 0, 0, 0)) } It 'Returns the friday 3rd' { $exp = '0 0 * 1 FRI' - $cron = ConvertFrom-PodeCronExpressions -Expressions $exp + $cron = ConvertFrom-PodeCronExpression -Expression $exp Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should -Be ([datetime]::new(2020, 1, 3, 0, 0, 0)) } It 'Returns the 2023 friday' { $exp = '0 0 13 1 FRI' - $cron = ConvertFrom-PodeCronExpressions -Expressions $exp + $cron = ConvertFrom-PodeCronExpression -Expression $exp Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should -Be ([datetime]::new(2023, 1, 13, 0, 0, 0)) } It 'Returns the null for after end time' { $exp = '0 0 20 1 FRI' $end = [datetime]::new(2020, 1, 19) - $cron = ConvertFrom-PodeCronExpressions -Expressions $exp + $cron = ConvertFrom-PodeCronExpression -Expression $exp Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate -EndTime $end | Should -Be $null } } @@ -555,37 +556,37 @@ Describe 'Get-PodeCronNextTrigger' { } It 'Returns the minute but next hour' { $exp = '20 * * * *' - $cron = ConvertFrom-PodeCronExpressions -Expressions $exp + $cron = ConvertFrom-PodeCronExpression -Expression $exp Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should -Be ([datetime]::new(2020, 1, 15, 3, 20, 0)) } It 'Returns the later minute but same hour' { $exp = '20,40 * * * *' - $cron = ConvertFrom-PodeCronExpressions -Expressions $exp + $cron = ConvertFrom-PodeCronExpression -Expression $exp Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should -Be ([datetime]::new(2020, 1, 15, 2, 40, 0)) } It 'Returns the next minute but same hour' { $exp = '20-40 * * * *' - $cron = ConvertFrom-PodeCronExpressions -Expressions $exp + $cron = ConvertFrom-PodeCronExpression -Expression $exp Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should -Be ([datetime]::new(2020, 1, 15, 2, 31, 0)) } It 'Returns the a very specific date' { $exp = '37 13 5 2 FRI' - $cron = ConvertFrom-PodeCronExpressions -Expressions $exp + $cron = ConvertFrom-PodeCronExpression -Expression $exp Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should -Be ([datetime]::new(2021, 2, 5, 13, 37, 0)) } It 'Returns the 30 March' { $inputDate = [datetime]::new(2020, 1, 31, 0, 0, 0) - $cron = ConvertFrom-PodeCronExpressions -Expressions '* * 30 * *' + $cron = ConvertFrom-PodeCronExpression -Expression '* * 30 * *' Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should -Be ([datetime]::new(2020, 3, 30, 0, 0, 0)) } It 'Returns the 28 Feb' { $inputDate = [datetime]::new(2020, 1, 31, 0, 0, 0) - $cron = ConvertFrom-PodeCronExpressions -Expressions '* * 28 * *' + $cron = ConvertFrom-PodeCronExpression -Expression '* * 28 * *' Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should -Be ([datetime]::new(2020, 2, 28, 0, 0, 0)) } } @@ -597,19 +598,19 @@ Describe 'Get-PodeCronNextEarliestTrigger' { } It 'Returns the earliest trigger when both valid' { - $crons = ConvertFrom-PodeCronExpressions -Expressions '* * 11 * FRI', '* * 10 * WED' + $crons = ConvertFrom-PodeCronExpression -Expression '* * 11 * FRI', '* * 10 * WED' Get-PodeCronNextEarliestTrigger -Expressions $crons -StartTime $inputDate | Should -Be ([datetime]::new(2020, 6, 10, 0, 0, 0)) } It 'Returns the earliest trigger when one after end time' { $end = [datetime]::new(2020, 1, 9) - $crons = ConvertFrom-PodeCronExpressions -Expressions '* * 8 * WED', '* * 10 * FRi' + $crons = ConvertFrom-PodeCronExpression -Expression '* * 8 * WED', '* * 10 * FRi' Get-PodeCronNextEarliestTrigger -Expressions $crons -StartTime $inputDate -EndTime $end | Should -Be ([datetime]::new(2020, 1, 8, 0, 0, 0)) } It 'Returns the null when all after end time' { $end = [datetime]::new(2020, 1, 7) - $crons = ConvertFrom-PodeCronExpressions -Expressions '* * 8 * WED', '* * 10 * FRi' + $crons = ConvertFrom-PodeCronExpression -Expression '* * 8 * WED', '* * 10 * FRi' Get-PodeCronNextEarliestTrigger -Expressions $crons -StartTime $inputDate -EndTime $end | Should -Be $null } } \ No newline at end of file diff --git a/tests/unit/Cryptography.Tests.ps1 b/tests/unit/Cryptography.Tests.ps1 index 1345986c5..423446f27 100644 --- a/tests/unit/Cryptography.Tests.ps1 +++ b/tests/unit/Cryptography.Tests.ps1 @@ -2,6 +2,7 @@ BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' } Describe 'Invoke-PodeHMACSHA256Hash' { @@ -15,11 +16,11 @@ Describe 'Invoke-PodeHMACSHA256Hash' { Describe 'Invoke-PodeSHA256Hash' { Context 'Invalid parameters supplied' { It 'Throws null value error' { - { Invoke-PodeSHA256Hash -Value $null } | Should -Throw -ExpectedMessage '*argument is null or empty*' + { Invoke-PodeSHA256Hash -Value $null } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Invoke-PodeSHA256Hash' } It 'Throws empty value error' { - { Invoke-PodeSHA256Hash -Value '' } | Should -Throw -ExpectedMessage '*argument is null or empty*' + { Invoke-PodeSHA256Hash -Value '' } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Invoke-PodeSHA256Hash' } } @@ -36,14 +37,14 @@ Describe 'New-PodeGuid' { } It 'Returns a secure guid' { - Mock Get-PodeRandomBytes { return @(10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10) } + Mock Get-PodeRandomByte { return @(10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10) } New-PodeGuid -Secure -Length 16 | Should -Be '0a0a0a0a-0a0a-0a0a-0a0a-0a0a0a0a0a0a' } } -Describe 'Get-PodeRandomBytes' { +Describe 'Get-PodeRandomByte' { It 'Returns an array of bytes' { - $b = (Get-PodeRandomBytes -Length 16) + $b = (Get-PodeRandomByte -Length 16) $b | Should -Not -Be $null $b.Length | Should -Be 16 } @@ -51,7 +52,7 @@ Describe 'Get-PodeRandomBytes' { Describe 'New-PodeSalt' { It 'Returns a salt' { - Mock Get-PodeRandomBytes { return @(10, 10, 10) } + Mock Get-PodeRandomByte { return @(10, 10, 10) } New-PodeSalt -Length 3 | Should -Be 'CgoK' } } \ No newline at end of file diff --git a/tests/unit/Endware.Tests.ps1 b/tests/unit/Endware.Tests.ps1 index ba7458420..c3e80ae0c 100644 --- a/tests/unit/Endware.Tests.ps1 +++ b/tests/unit/Endware.Tests.ps1 @@ -1,7 +1,10 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] +param() BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' } Describe 'Invoke-PodeEndware' { @@ -38,7 +41,7 @@ Describe 'Invoke-PodeEndware' { Describe 'Add-PodeEndware' { Context 'Invalid parameters supplied' { It 'Throws null logic error' { - { Add-PodeEndware -ScriptBlock $null } | Should -Throw -ExpectedMessage '*because it is null*' + { Add-PodeEndware -ScriptBlock $null } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorNullNotAllowed,Add-PodeEndware' } } diff --git a/tests/unit/Flash.Tests.ps1 b/tests/unit/Flash.Tests.ps1 index 98b613611..3996553c2 100644 --- a/tests/unit/Flash.Tests.ps1 +++ b/tests/unit/Flash.Tests.ps1 @@ -5,22 +5,23 @@ BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' } Describe 'Add-PodeFlashMessage' { It 'Throws error because sessions are not configured' { $PodeContext = @{ 'Server' = @{ 'Sessions' = @{} } } - { Add-PodeFlashMessage -Name 'name' -Message 'message' } | Should -Throw -ExpectedMessage '*Sessions are required*' + { Add-PodeFlashMessage -Name 'name' -Message 'message' } | Should -Throw -ExpectedMessage $PodeLocale.sessionsRequiredForFlashMessagesExceptionMessage #'*Sessions are required*' } It 'Throws error for no name supplied' { $PodeContext = @{ 'Server' = @{ 'Sessions' = @{ 'Secret' = 'Key' } } } - { Add-PodeFlashMessage -Name '' -Message 'message' } | Should -Throw -ExpectedMessage '*empty string*' + { Add-PodeFlashMessage -Name '' -Message 'message' } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorEmptyStringNotAllowed,Add-PodeFlashMessage' } It 'Throws error for no message supplied' { $PodeContext = @{ 'Server' = @{ 'Sessions' = @{ 'Secret' = 'Key' } } } - { Add-PodeFlashMessage -Name 'name' -Message '' } | Should -Throw -ExpectedMessage '*empty string*' + { Add-PodeFlashMessage -Name 'name' -Message '' } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorEmptyStringNotAllowed,Add-PodeFlashMessage' } It 'Adds a single key and value' { @@ -65,7 +66,7 @@ Describe 'Add-PodeFlashMessage' { Describe 'Clear-PodeFlashMessages' { It 'Throws error because sessions are not configured' { $PodeContext = @{ 'Server' = @{ 'Sessions' = @{} } } - { Clear-PodeFlashMessages } | Should -Throw -ExpectedMessage '*Sessions are required*' + { Clear-PodeFlashMessages } | Should -Throw -ExpectedMessage $PodeLocale.sessionsRequiredForFlashMessagesExceptionMessage #'*Sessions are required*' } It 'Adds two keys and then Clears them all' { @@ -88,12 +89,12 @@ Describe 'Clear-PodeFlashMessages' { Describe 'Get-PodeFlashMessage' { It 'Throws error because sessions are not configured' { $PodeContext = @{ 'Server' = @{ 'Sessions' = @{} } } - { Get-PodeFlashMessage -Name 'name' } | Should -Throw -ExpectedMessage '*Sessions are required*' + { Get-PodeFlashMessage -Name 'name' } | Should -Throw -ExpectedMessage $PodeLocale.sessionsRequiredForFlashMessagesExceptionMessage #'*Sessions are required*' } It 'Throws error for no key supplied' { $PodeContext = @{ 'Server' = @{ 'Sessions' = @{ 'Secret' = 'Key' } } } - { Get-PodeFlashMessage -Name '' } | Should -Throw -ExpectedMessage '*empty string*' + { Get-PodeFlashMessage -Name '' } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorEmptyStringNotAllowed,Get-PodeFlashMessage' } It 'Returns empty array on key that does not exist' { @@ -156,7 +157,7 @@ Describe 'Get-PodeFlashMessage' { Describe 'Get-PodeFlashMessageNames' { It 'Throws error because sessions are not configured' { $PodeContext = @{ 'Server' = @{ 'Sessions' = @{} } } - { Get-PodeFlashMessageNames } | Should -Throw -ExpectedMessage '*Sessions are required*' + { Get-PodeFlashMessageNames } | Should -Throw -ExpectedMessage $PodeLocale.sessionsRequiredForFlashMessagesExceptionMessage #'*Sessions are required*' } It 'Adds two keys and then retrieves the Keys' { @@ -191,12 +192,12 @@ Describe 'Get-PodeFlashMessageNames' { Describe 'Remove-PodeFlashMessage' { It 'Throws error because sessions are not configured' { $PodeContext = @{ 'Server' = @{ 'Sessions' = @{} } } - { Remove-PodeFlashMessage -Name 'name' } | Should -Throw -ExpectedMessage '*Sessions are required*' + { Remove-PodeFlashMessage -Name 'name' } | Should -Throw -ExpectedMessage $PodeLocale.sessionsRequiredForFlashMessagesExceptionMessage #'*Sessions are required*' } It 'Throws error for no key supplied' { $PodeContext = @{ 'Server' = @{ 'Sessions' = @{ 'Secret' = 'Key' } } } - { Remove-PodeFlashMessage -Name '' } | Should -Throw -ExpectedMessage '*empty string*' + { Remove-PodeFlashMessage -Name '' } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorEmptyStringNotAllowed,Remove-PodeFlashMessage' } It 'Adds two keys and then Remove one of them' { @@ -219,12 +220,12 @@ Describe 'Remove-PodeFlashMessage' { Describe 'Test-PodeFlashMessage' { It 'Throws error because sessions are not configured' { $PodeContext = @{ 'Server' = @{ 'Sessions' = @{} } } - { Test-PodeFlashMessage -Name 'name' } | Should -Throw -ExpectedMessage '*Sessions are required*' + { Test-PodeFlashMessage -Name 'name' } | Should -Throw -ExpectedMessage $PodeLocale.sessionsRequiredForFlashMessagesExceptionMessage #'*Sessions are required*' } It 'Throws error for no key supplied' { $PodeContext = @{ 'Server' = @{ 'Sessions' = @{ 'Secret' = 'Key' } } } - { Test-PodeFlashMessage -Name '' } | Should -Throw -ExpectedMessage '*empty string*' + { Test-PodeFlashMessage -Name '' } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorEmptyStringNotAllowed,Test-PodeFlashMessage' } It 'Adds two keys and then Tests if one of them exists' { diff --git a/tests/unit/Handlers.Tests.ps1 b/tests/unit/Handlers.Tests.ps1 index 02d4d4bc7..29dd6ce09 100644 --- a/tests/unit/Handlers.Tests.ps1 +++ b/tests/unit/Handlers.Tests.ps1 @@ -1,17 +1,19 @@ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] param() BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' $PodeContext = @{ 'Server' = $null; } } Describe 'Get-PodeHandler' { Context 'Invalid parameters supplied' { It 'Throw invalid type error' { - { Get-PodeHandler -Type 'Moo' } | Should -Throw -ExpectedMessage "*Cannot validate argument on parameter 'Type'*" + { Get-PodeHandler -Type 'Moo' } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Get-PodeHandler' } } @@ -70,7 +72,9 @@ Describe 'Get-PodeHandler' { Describe 'Add-PodeHandler' { It 'Throws error because type already exists' { $PodeContext.Server = @{ 'Handlers' = @{ 'Smtp' = @{ 'Main' = @{}; }; }; } - { Add-PodeHandler -Type Smtp -Name 'Main' -ScriptBlock {} } | Should -Throw -ExpectedMessage '*already defined*' + $expectedMessage = ($PodeLocale.handlerAlreadyDefinedExceptionMessage -f 'Smtp', 'Main').Replace('[', '`[').Replace(']', '`]') # -replace '\[', '`[' -replace '\]', '`]' + { Add-PodeHandler -Type Smtp -Name 'Main' -ScriptBlock {} } | Should -Throw -ExpectedMessage $expectedMessage #'*already defined*' + } It 'Adds smtp handler' { diff --git a/tests/unit/Headers.Tests.ps1 b/tests/unit/Headers.Tests.ps1 index 003671688..a666ce642 100644 --- a/tests/unit/Headers.Tests.ps1 +++ b/tests/unit/Headers.Tests.ps1 @@ -4,6 +4,7 @@ BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' } Describe 'Test-PodeHeader' { Context 'WebServer' { diff --git a/tests/unit/Helpers.Tests.ps1 b/tests/unit/Helpers.Tests.ps1 index d80e5c543..76c2c716a 100644 --- a/tests/unit/Helpers.Tests.ps1 +++ b/tests/unit/Helpers.Tests.ps1 @@ -1,9 +1,12 @@ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] param() BeforeAll { + Add-Type -AssemblyName 'System.Net.Http' -ErrorAction SilentlyContinue $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' } Describe 'Get-PodeType' { @@ -258,7 +261,7 @@ Describe 'Test-PodeIPAddress' { Describe 'ConvertTo-PodeIPAddress' { Context 'Null values' { It 'Throws error for null' { - { ConvertTo-PodeIPAddress -Address $null } | Should -Throw -ExpectedMessage '*the argument is null*' + { ConvertTo-PodeIPAddress -Address $null } | Should -Throw -ErrorId 'ParameterArgumentValidationError,ConvertTo-PodeIPAddress' } } @@ -283,11 +286,11 @@ Describe 'ConvertTo-PodeIPAddress' { Describe 'Test-PodeIPAddressLocal' { Context 'Null values' { It 'Throws error for empty' { - { Test-PodeIPAddressLocal -IP ([string]::Empty) } | Should -Throw -ExpectedMessage '*because it is an empty*' + { Test-PodeIPAddressLocal -IP ([string]::Empty) } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorEmptyStringNotAllowed,Test-PodeIPAddressLocal' } It 'Throws error for null' { - { Test-PodeIPAddressLocal -IP $null } | Should -Throw -ExpectedMessage '*because it is an empty*' + { Test-PodeIPAddressLocal -IP $null } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorEmptyStringNotAllowed,Test-PodeIPAddressLocal' } } @@ -311,11 +314,11 @@ Describe 'Test-PodeIPAddressLocal' { Describe 'Test-PodeIPAddressLocalOrAny' { Context 'Null values' { It 'Throws error for empty' { - { Test-PodeIPAddressLocalOrAny -IP ([string]::Empty) } | Should -Throw -ExpectedMessage '*because it is an empty*' + { Test-PodeIPAddressLocalOrAny -IP ([string]::Empty) } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorEmptyStringNotAllowed,Test-PodeIPAddressLocalOrAny' } It 'Throws error for null' { - { Test-PodeIPAddressLocalOrAny -IP $null } | Should -Throw -ExpectedMessage '*because it is an empty*' + { Test-PodeIPAddressLocalOrAny -IP $null } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorEmptyStringNotAllowed,Test-PodeIPAddressLocalOrAny' } } @@ -347,11 +350,11 @@ Describe 'Test-PodeIPAddressLocalOrAny' { Describe 'Test-PodeIPAddressAny' { Context 'Null values' { It 'Throws error for empty' { - { Test-PodeIPAddressAny -IP ([string]::Empty) } | Should -Throw -ExpectedMessage '*because it is an empty*' + { Test-PodeIPAddressAny -IP ([string]::Empty) } | Should -ErrorId 'ParameterArgumentValidationErrorEmptyStringNotAllowed,Test-PodeIPAddressAny' } It 'Throws error for null' { - { Test-PodeIPAddressAny -IP $null } | Should -Throw -ExpectedMessage '*because it is an empty*' + { Test-PodeIPAddressAny -IP $null } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorEmptyStringNotAllowed,Test-PodeIPAddressAny' } } @@ -401,7 +404,7 @@ Describe 'Get-PodeIPAddress' { } It 'Throws error for invalid IP' { - { Get-PodeIPAddress -IP '~fake.net' } | Should -Throw -ExpectedMessage '*invalid IP address*' + { Get-PodeIPAddress -IP '~fake.net' } | Should -Throw -ErrorId 'FormatException,Get-PodeIPAddress' } } @@ -411,7 +414,7 @@ Describe 'Get-PodeIPAddress' { } It 'Throws error for invalid IP' { - { Get-PodeIPAddress -IP '256.0.0.0' } | Should -Throw -ExpectedMessage '*invalid IP address*' + { Get-PodeIPAddress -IP '256.0.0.0' } | Should -Throw -ErrorId 'FormatException,Get-PodeIPAddress' } } @@ -425,7 +428,7 @@ Describe 'Get-PodeIPAddress' { } It 'Throws error for invalid IP' { - { Get-PodeIPAddress -IP '[]' } | Should -Throw -ExpectedMessage '*invalid IP address*' + { Get-PodeIPAddress -IP '[]' } | Should -Throw -ErrorId 'FormatException,Get-PodeIPAddress' } } } @@ -433,15 +436,15 @@ Describe 'Get-PodeIPAddress' { Describe 'Test-PodeIPAddressInRange' { Context 'No parameters supplied' { It 'Throws error for no ip' { - { Test-PodeIPAddressInRange -IP $null -LowerIP @{} -UpperIP @{} } | Should -Throw -ExpectedMessage '*because it is null*' + { Test-PodeIPAddressInRange -IP $null -LowerIP @{} -UpperIP @{} } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorNullNotAllowed,Test-PodeIPAddressInRange' } It 'Throws error for no lower ip' { - { Test-PodeIPAddressInRange -IP @{} -LowerIP $null -UpperIP @{} } | Should -Throw -ExpectedMessage '*because it is null*' + { Test-PodeIPAddressInRange -IP @{} -LowerIP $null -UpperIP @{} } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorNullNotAllowed,Test-PodeIPAddressInRange' } It 'Throws error for no upper ip' { - { Test-PodeIPAddressInRange -IP @{} -LowerIP @{} -UpperIP $null } | Should -Throw -ExpectedMessage '*because it is null*' + { Test-PodeIPAddressInRange -IP @{} -LowerIP @{} -UpperIP $null } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorNullNotAllowed,Test-PodeIPAddressInRange' } } @@ -500,11 +503,11 @@ Describe 'Test-PodeIPAddressInRange' { Describe 'Test-PodeIPAddressIsSubnetMask' { Context 'Null values' { It 'Throws error for empty' { - { Test-PodeIPAddressIsSubnetMask -IP ([string]::Empty) } | Should -Throw -ExpectedMessage '*argument is null or empty*' + { Test-PodeIPAddressIsSubnetMask -IP ([string]::Empty) } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Test-PodeIPAddressIsSubnetMask' } It 'Throws error for null' { - { Test-PodeIPAddressIsSubnetMask -IP $null } | Should -Throw -ExpectedMessage '*argument is null or empty*' + { Test-PodeIPAddressIsSubnetMask -IP $null } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Test-PodeIPAddressIsSubnetMask' } } @@ -720,11 +723,11 @@ Describe 'Test-PodePathIsWildcard' { Describe 'Test-PodePathIsDirectory' { Context 'Null values' { It 'Throws error for empty' { - { Test-PodePathIsDirectory -Path ([string]::Empty) } | Should -Throw -ExpectedMessage '*argument is null or empty*' + { Test-PodePathIsDirectory -Path ([string]::Empty) } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Test-PodePathIsDirectory' } It 'Throws error for null' { - { Test-PodePathIsDirectory -Path $null } | Should -Throw -ExpectedMessage '*argument is null or empty*' + { Test-PodePathIsDirectory -Path $null } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Test-PodePathIsDirectory' } } @@ -767,15 +770,15 @@ Describe 'Get-PodeEndpointInfo' { } It 'Throws an error for an invalid IP endpoint' { - { Get-PodeEndpointInfo -Address '700.0.0.a' } | Should -Throw -ExpectedMessage '*Failed to parse*' + { Get-PodeEndpointInfo -Address '700.0.0.a' } | Should -Throw -ExpectedMessage ($PodeLocale.failedToParseAddressExceptionMessage -f '700.0.0.a' ) #'*Failed to parse*' } It 'Throws an error for an out-of-range IP endpoint' { - { Get-PodeEndpointInfo -Address '700.0.0.0' } | Should -Throw -ExpectedMessage '*The IP address supplied is invalid*' + { Get-PodeEndpointInfo -Address '700.0.0.0' } | Should -Throw -ExpectedMessage ($PodeLocale.invalidIpAddressExceptionMessage -f '700.0.0.0' ) # '*The IP address supplied is invalid*' } It 'Throws an error for an invalid Hostname endpoint' { - { Get-PodeEndpointInfo -Address '@test.host.com' } | Should -Throw -ExpectedMessage '*Failed to parse*' + { Get-PodeEndpointInfo -Address '@test.host.com' } | Should -Throw -ExpectedMessage ($PodeLocale.failedToParseAddressExceptionMessage -f '@test.host.com') # '*Failed to parse*' } } @@ -1074,11 +1077,11 @@ Describe 'Get-PodeRelativePath' { It 'Throws error for path ot existing' { Mock Test-PodePath { return $false } - { Get-PodeRelativePath -Path './path' -TestPath } | Should -Throw -ExpectedMessage '*The path does not exist*' + { Get-PodeRelativePath -Path './path' -TestPath } | Should -Throw -ExpectedMessage ($PodeLocale.pathNotExistExceptionMessage -f './path') # '*The path does not exist*' } } -Describe 'Get-PodeWildcardFiles' { +Describe 'Get-PodeWildcardFile' { BeforeAll { Mock Get-PodeRelativePath { return $Path } Mock Get-ChildItem { @@ -1088,19 +1091,19 @@ Describe 'Get-PodeWildcardFiles' { } It 'Get files after adding a wildcard to a directory' { - $result = @(Get-PodeWildcardFiles -Path './path' -Wildcard '*.ps1') + $result = @(Get-PodeWildcardFile -Path './path' -Wildcard '*.ps1') $result.Length | Should -Be 1 $result[0] | Should -Be './file1.ps1' } It 'Get files for wildcard path' { - $result = @(Get-PodeWildcardFiles -Path './path/*.png') + $result = @(Get-PodeWildcardFile -Path './path/*.png') $result.Length | Should -Be 1 $result[0] | Should -Be './file1.png' } It 'Returns null for non-wildcard path' { - Get-PodeWildcardFiles -Path './some/path/file.txt' | Should -Be $null + Get-PodeWildcardFile -Path './some/path/file.txt' | Should -Be $null } } @@ -1117,28 +1120,28 @@ Describe 'Test-PodeIsServerless' { It 'Throws error if serverless' { $PodeContext = @{ 'Server' = @{ 'IsServerless' = $true } } - { Test-PodeIsServerless -ThrowError } | Should -Throw -ExpectedMessage '*not supported in a serverless*' + { Test-PodeIsServerless -FunctionName 'FakeFunction' -ThrowError } | Should -Throw -ExpectedMessage ($PodeLocale.unsupportedFunctionInServerlessContextExceptionMessage -f 'FakeFunction') #'*not supported in a serverless*' } It 'Throws no error if not serverless' { $PodeContext = @{ 'Server' = @{ 'IsServerless' = $false } } - { Test-PodeIsServerless -ThrowError } | Should -Not -Throw -ExpectedMessage '*not supported in a serverless*' + { Test-PodeIsServerless -FunctionName 'FakeFunction' -ThrowError } | Should -Not -Throw -ExpectedMessage ($PodeLocale.unsupportedFunctionInServerlessContextExceptionMessage -f 'FakeFunction') #'*not supported in a serverless*' } } -Describe 'Close-PodeRunspaces' { +Describe 'Close-PodeRunspace' { It 'Returns and does nothing if serverless' { $PodeContext = @{ 'Server' = @{ 'IsServerless' = $true } } - Close-PodeRunspaces -ClosePool + Close-PodeRunspace -ClosePool } } Describe 'Close-PodeServerInternal' { BeforeAll { - Mock Close-PodeRunspaces { } + Mock Close-PodeRunspace { } Mock Stop-PodeFileMonitor { } Mock Close-PodeDisposable { } - Mock Remove-PodePSDrives { } + Mock Remove-PodePSDrive { } Mock Write-Host { } } It 'Closes out pode, but with no done flag' { @@ -1287,60 +1290,6 @@ Describe 'Get-PodeCount' { } } -Describe 'Convert-PodePathSeparators' { - Context 'Null' { - It 'Null' { - Convert-PodePathSeparators -Path $null | Should -Be $null - } - } - - Context 'String' { - It 'Empty' { - Convert-PodePathSeparators -Path '' | Should -Be $null - Convert-PodePathSeparators -Path ' ' | Should -Be $null - } - - It 'Value' { - Convert-PodePathSeparators -Path 'anyValue' | Should -Be 'anyValue' - Convert-PodePathSeparators -Path 1 | Should -Be 1 - } - - It 'Path' { - Convert-PodePathSeparators -Path 'one/Seperators' | Should -Be "one$([System.IO.Path]::DirectorySeparatorChar)Seperators" - Convert-PodePathSeparators -Path 'one\Seperators' | Should -Be "one$([System.IO.Path]::DirectorySeparatorChar)Seperators" - - Convert-PodePathSeparators -Path 'one/two/Seperators' | Should -Be "one$([System.IO.Path]::DirectorySeparatorChar)two$([System.IO.Path]::DirectorySeparatorChar)Seperators" - Convert-PodePathSeparators -Path 'one\two\Seperators' | Should -Be "one$([System.IO.Path]::DirectorySeparatorChar)two$([System.IO.Path]::DirectorySeparatorChar)Seperators" - Convert-PodePathSeparators -Path 'one/two\Seperators' | Should -Be "one$([System.IO.Path]::DirectorySeparatorChar)two$([System.IO.Path]::DirectorySeparatorChar)Seperators" - Convert-PodePathSeparators -Path 'one\two/Seperators' | Should -Be "one$([System.IO.Path]::DirectorySeparatorChar)two$([System.IO.Path]::DirectorySeparatorChar)Seperators" - } - } - - Context 'Array' { - It 'Null' { - Convert-PodePathSeparators -Path @($null) | Should -Be $null - Convert-PodePathSeparators -Path @($null, $null) | Should -Be $null - } - - It 'Single' { - Convert-PodePathSeparators -Path @('noSeperators') | Should -Be @('noSeperators') - Convert-PodePathSeparators -Path @('some/Seperators') | Should -Be @("some$([System.IO.Path]::DirectorySeparatorChar)Seperators") - Convert-PodePathSeparators -Path @('some\Seperators') | Should -Be @("some$([System.IO.Path]::DirectorySeparatorChar)Seperators") - - Convert-PodePathSeparators -Path @('') | Should -Be $null - Convert-PodePathSeparators -Path @(' ') | Should -Be $null - } - - It 'Double' { - Convert-PodePathSeparators -Path @('noSeperators1', 'noSeperators2') | Should -Be @('noSeperators1', 'noSeperators2') - Convert-PodePathSeparators -Path @('some/Seperators', 'some\Seperators') | Should -Be @("some$([System.IO.Path]::DirectorySeparatorChar)Seperators", "some$([System.IO.Path]::DirectorySeparatorChar)Seperators") - - Convert-PodePathSeparators -Path @('', ' ') | Should -Be $null - Convert-PodePathSeparators -Path @(' ', '') | Should -Be $null - } - } -} - Describe 'Out-PodeHost' { BeforeAll { Mock Out-Default {} @@ -1359,24 +1308,10 @@ Describe 'Out-PodeHost' { @{ Name = 'Rick' } | Out-PodeHost Assert-MockCalled Out-Default -Scope It -Times 1 } -} -Describe 'Remove-PodeNullKeysFromHashtable' { - It 'Removes all null values keys' { - $ht = @{ - Value1 = $null - Value2 = @{ - Value3 = @() - Value4 = $null - } - } - - $ht | Remove-PodeNullKeysFromHashtable - - $ht.ContainsKey('Value1') | Should -Be $false - $ht.ContainsKey('Value2') | Should -Be $true - $ht.Value2.ContainsKey('Value3') | Should -Be $true - $ht.Value2.ContainsKey('Value4') | Should -Be $false + It 'Writes an Array to the Host by pipeline' { + @('France','Rick',21 ,'male') | Out-PodeHost + Assert-MockCalled Out-Default -Scope It -Times 1 } } @@ -1564,12 +1499,12 @@ Describe 'Get-PodeAcceptEncoding' { It 'Errors when no encoding matches, and identity disabled' { $PodeContext.Server.Web.Compression.Enabled = $true - { Get-PodeAcceptEncoding -AcceptEncoding 'br,identity;q=0' -ThrowError } | Should -Throw -ExpectedMessage '*HttpRequestException*' + { Get-PodeAcceptEncoding -AcceptEncoding 'br,identity;q=0' -ThrowError } | Should -Throw -ExceptionType 'System.Net.Http.HttpRequestException' } It 'Errors when no encoding matches, and wildcard disabled' { $PodeContext.Server.Web.Compression.Enabled = $true - { Get-PodeAcceptEncoding -AcceptEncoding 'br,*;q=0' -ThrowError } | Should -Throw -ExpectedMessage '*HttpRequestException*' + { Get-PodeAcceptEncoding -AcceptEncoding 'br,*;q=0' -ThrowError } | Should -Throw -ExceptionType 'System.Net.Http.HttpRequestException' } It 'Returns empty if identity is allowed, but wildcard disabled' { @@ -1607,7 +1542,7 @@ Describe 'Get-PodeTransferEncoding' { } It 'Errors when no encoding matches' { - { Get-PodeTransferEncoding -TransferEncoding 'compress,chunked' -ThrowError } | Should -Throw -ExpectedMessage '*HttpRequestException*' + { Get-PodeTransferEncoding -TransferEncoding 'compress,chunked' -ThrowError } | Should -Throw -ExceptionType 'System.Net.Http.HttpRequestException' } } @@ -1695,43 +1630,107 @@ Describe 'New-PodeCron' { } It 'Throws an error for multiple Hours when using Interval' { - { New-PodeCron -Every Hour -Hour 2, 4 -Interval 3 } | Should -Throw -ExpectedMessage '*only supply a single*' + { New-PodeCron -Every Hour -Hour 2, 4 -Interval 3 } | Should -Throw -ExpectedMessage ($PodeLocale.singleValueForIntervalExceptionMessage -f 'Hour') #'*only supply a single*' } It 'Throws an error for multiple Minutes when using Interval' { - { New-PodeCron -Every Minute -Minute 2, 4 -Interval 15 } | Should -Throw -ExpectedMessage '*only supply a single*' + { New-PodeCron -Every Minute -Minute 2, 4 -Interval 15 } | Should -Throw -ExpectedMessage ($PodeLocale.singleValueForIntervalExceptionMessage -f 'Minute') #'*only supply a single*' } It 'Throws an error when using Interval without Every' { - { New-PodeCron -Interval 3 } | Should -Throw -ExpectedMessage '*Cannot supply an interval*' + { New-PodeCron -Interval 3 } | Should -Throw -ExpectedMessage $PodeLocale.cannotSupplyIntervalWhenEveryIsNoneExceptionMessage #'*Cannot supply an interval*' } It 'Throws an error when using Interval for Every Quarter' { - { New-PodeCron -Every Quarter -Interval 3 } | Should -Throw -ExpectedMessage 'Cannot supply interval value for every quarter' + { New-PodeCron -Every Quarter -Interval 3 } | Should -Throw -ExpectedMessage $PodeLocale.cannotSupplyIntervalForQuarterExceptionMessage #Cannot supply interval value for every quarter. } It 'Throws an error when using Interval for Every Year' { - { New-PodeCron -Every Year -Interval 3 } | Should -Throw -ExpectedMessage 'Cannot supply interval value for every year' + { New-PodeCron -Every Year -Interval 3 } | Should -Throw -ExpectedMessage $PodeLocale.cannotSupplyIntervalForYearExceptionMessage #'Cannot supply interval value for every year' } } + +Describe 'ConvertTo-PodeYaml Tests' { + BeforeAll { + $PodeContext = @{ + Server = @{ + InternalCache = @{} + Web = @{ + OpenApi = @{ + UsePodeYamlInternal = $true + } + } + } + } + } + + Context 'When converting basic types' { + It 'Converts strings correctly' { + $result = 'hello world' | ConvertTo-PodeYaml + $result | Should -Be 'hello world' + } + + It 'Converts arrays correctly' { + $result = @('one', 'two', 'three') | ConvertTo-PodeYaml + $expected = (@' +- one +- two +- three +'@) + $result | Should -Be ($expected.Trim() -Replace "`r`n", "`n") + } + + It 'Converts hashtables correctly' { + $hashTable = [ordered]@{ + key1 = 'value1' + key2 = 'value2' + } + $result = $hashTable | ConvertTo-PodeYaml + $result | Should -Be "key1: value1`nkey2: value2" + } + } + + Context 'When converting complex objects' { + It 'Handles nested hashtables' { + $nestedHash = @{ + parent = @{ + child = 'value' + } + } + $result = $nestedHash | ConvertTo-PodeYaml + + $result | Should -Be "parent: `n child: value" + } + } + + Context 'Error handling' { + It 'Returns empty string for null input' { + $result = $null | ConvertTo-PodeYaml + $result | Should -Be '' + } + } +} + + Describe 'ConvertTo-PodeYamlInternal Tests' { Context 'When converting basic types' { It 'Converts strings correctly' { - $result = 'hello world' | ConvertTo-PodeYamlInternal + + $result = ConvertTo-PodeYamlInternal -InputObject 'hello world' $result | Should -Be 'hello world' } It 'Converts arrays correctly' { - $result = ConvertTo-PodeYamlInternal -InputObject @('one', 'two', 'three') -NoNewLine + $result = ConvertTo-PodeYamlInternal -InputObject @('one', 'two', 'three') -NoNewLine $expected = (@' - one - two - three '@) - $result | Should -Be ($expected.Trim() -Replace "`r`n","`n") + $result | Should -Be ($expected.Trim() -Replace "`r`n", "`n") } It 'Converts hashtables correctly' { @@ -1739,7 +1738,7 @@ Describe 'ConvertTo-PodeYamlInternal Tests' { key1 = 'value1' key2 = 'value2' } - $result = $hashTable | ConvertTo-PodeYamlInternal -NoNewLine + $result = ConvertTo-PodeYamlInternal -InputObject $hashTable -NoNewLine $result | Should -Be "key1: value1`nkey2: value2" } } @@ -1751,7 +1750,7 @@ Describe 'ConvertTo-PodeYamlInternal Tests' { child = 'value' } } - $result = $nestedHash | ConvertTo-PodeYamlInternal -NoNewLine + $result = ConvertTo-PodeYamlInternal -InputObject $nestedHash -NoNewLine $result | Should -Be "parent: `n child: value" } @@ -1759,7 +1758,7 @@ Describe 'ConvertTo-PodeYamlInternal Tests' { Context 'Error handling' { It 'Returns empty string for null input' { - $result = $null | ConvertTo-PodeYamlInternal + $result = ConvertTo-PodeYamlInternal -InputObject $null $result | Should -Be '' } } diff --git a/tests/unit/Localization.Tests.ps1 b/tests/unit/Localization.Tests.ps1 new file mode 100644 index 000000000..593d3d215 --- /dev/null +++ b/tests/unit/Localization.Tests.ps1 @@ -0,0 +1,78 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() +BeforeDiscovery { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src' + + # All language directories + $localizationDir = "$src/Locales" + + # Discover all language directories + $languageDirs = (Get-ChildItem -Path $localizationDir -Directory | Where-Object { $_.Name -ne 'en' }).FullName + + # Get all source code files recursively from the specified directory + $sourceFiles = (Get-ChildItem -Path $src -Recurse -Include *.ps1, *.psm1).FullName + Import-LocalizedData -BindingVariable LanguageOfReference -BaseDirectory $localizationDir -FileName 'Pode' -UICulture 'en' +} + +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + # All language directories + $localizationDir = "$src/Locales" + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory $localizationDir -FileName 'Pode' +} + +Describe 'Localization Check' { + + + # Function to extract hashtable keys from a file + function Export-KeysFromFile { + param ( + [string]$filePath + ) + + $content = Get-Content -Path $filePath -Raw + $keys = @() + $regex = '\$PodeLocale\["([^"]+)"\]|\$PodeLocale\.([a-zA-Z_][a-zA-Z0-9_]*)' + foreach ($match in [regex]::Matches($content, $regex)) { + if ($match.Groups[1].Value) { + $keys += $match.Groups[1].Value + } + elseif ($match.Groups[2].Value) { + $keys += $match.Groups[2].Value + } + } + return $keys + } + + Describe 'Verify Invalid Hashtable Keys in [<_>]' -ForEach ($sourceFiles) { + $keysInFile = Export-KeysFromFile -filePath $_ + It "should find the key '[<_>]' in the hashtable" -ForEach ($keysInFile) { + $PodeLocale.Keys -contains $_ | Should -BeTrue + } + } + + It "Check 'throw' is not using a static string in [<_>]" -ForEach ($sourceFiles) { + ( Get-Content -Path $_ -Raw) -match 'throw\s*["\'']' | Should -BeFalse + } + + Describe 'Verifying Language [<_>]' -ForEach ($languageDirs) { + + BeforeAll { + $content = Import-LocalizedData -FileName 'Pode.psd1' -BaseDirectory $localizationDir -UICulture (Split-Path $_ -Leaf) + } + it 'Language resource file exist' { + Test-Path -Path "$($_)/Pode.psd1" | Should -BeTrue + } + + it 'Number of entry equal to the [en]' { + $content.Keys.Count | Should -be $PodeLocale.Count + } + + It -Name 'Resource File contains <_>' -ForEach ( $LanguageOfReference.Keys) { + $content.Keys -contains $_ | Should -BeTrue + } + } +} diff --git a/tests/unit/Logging.Tests.ps1 b/tests/unit/Logging.Tests.ps1 index 3d54a6a2a..1353ea076 100644 --- a/tests/unit/Logging.Tests.ps1 +++ b/tests/unit/Logging.Tests.ps1 @@ -4,6 +4,7 @@ BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' } Describe 'Get-PodeLogger' { It 'Returns null as the logger does not exist' { @@ -30,7 +31,7 @@ Describe 'Get-PodeLogger' { Describe 'Write-PodeLog' { It 'Does nothing when logging disabled' { Mock Test-PodeLoggerEnabled { return $false } - $PodeContext = @{ LogsToProcess = New-Object System.Collections.ArrayList } + $PodeContext = @{ LogsToProcess = [System.Collections.ArrayList]::new() } Write-PodeLog -Name 'test' -InputObject 'test' @@ -39,7 +40,7 @@ Describe 'Write-PodeLog' { It 'Adds a log item' { Mock Test-PodeLoggerEnabled { return $true } - $PodeContext = @{ LogsToProcess = New-Object System.Collections.ArrayList } + $PodeContext = @{ LogsToProcess = [System.Collections.ArrayList]::new() } Write-PodeLog -Name 'test' -InputObject 'test' @@ -52,7 +53,7 @@ Describe 'Write-PodeLog' { Describe 'Write-PodeErrorLog' { It 'Does nothing when logging disabled' { Mock Test-PodeLoggerEnabled { return $false } - $PodeContext = @{ LogsToProcess = New-Object System.Collections.ArrayList } + $PodeContext = @{ LogsToProcess = [System.Collections.ArrayList]::new() } Write-PodeLog -Name 'test' -InputObject 'test' @@ -66,7 +67,7 @@ Describe 'Write-PodeErrorLog' { } } } - $PodeContext = @{ LogsToProcess = New-Object System.Collections.ArrayList } + $PodeContext = @{ LogsToProcess = [System.Collections.ArrayList]::new() } try { throw 'some error' } catch { @@ -84,7 +85,7 @@ Describe 'Write-PodeErrorLog' { } } } - $PodeContext = @{ LogsToProcess = New-Object System.Collections.ArrayList } + $PodeContext = @{ LogsToProcess = [System.Collections.ArrayList]::new() } $exp = [exception]::new('some error') Write-PodeErrorLog -Exception $exp @@ -100,7 +101,7 @@ Describe 'Write-PodeErrorLog' { } } } - $PodeContext = @{ LogsToProcess = New-Object System.Collections.ArrayList } + $PodeContext = @{ LogsToProcess = [System.Collections.ArrayList]::new() } $exp = [exception]::new('some error') Write-PodeErrorLog -Exception $exp -Level Verbose diff --git a/tests/unit/Mappers.Tests.ps1 b/tests/unit/Mappers.Tests.ps1 index a8557fa3f..ab0446730 100644 --- a/tests/unit/Mappers.Tests.ps1 +++ b/tests/unit/Mappers.Tests.ps1 @@ -4,6 +4,7 @@ BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' } Describe 'Get-PodeContentType' { Context 'No extension supplied' { @@ -68,7 +69,7 @@ Describe 'Get-PodeContentType' { '.accdw' = 'application/msaccess.webapplication' '.accft' = 'application/msaccess.ftemplate' '.acx' = 'application/internet-property-stream' - '.addin' = 'text/xml' + '.addin' = 'application/xml' '.ade' = 'application/msaccess' '.adobebridge' = 'application/x-bridge-url' '.adp' = 'application/msaccess' @@ -152,10 +153,10 @@ Describe 'Get-PodeContentType' { '.dib' = 'image/bmp' '.dif' = 'video/x-dv' '.dir' = 'application/x-director' - '.disco' = 'text/xml' + '.disco' = 'application/xml' '.divx' = 'video/divx' '.dll' = 'application/x-msdownload' - '.dll.config' = 'text/xml' + '.dll.config' = 'application/xml' '.dlm' = 'text/dlm' '.doc' = 'application/msword' '.docm' = 'application/vnd.ms-word.document.macroEnabled.12' @@ -165,8 +166,8 @@ Describe 'Get-PodeContentType' { '.dotx' = 'application/vnd.openxmlformats-officedocument.wordprocessingml.template' '.dsp' = 'application/octet-stream' '.dsw' = 'text/plain' - '.dtd' = 'text/xml' - '.dtsconfig' = 'text/xml' + '.dtd' = 'application/xml' + '.dtsconfig' = 'application/xml' '.dv' = 'video/x-dv' '.dvi' = 'application/x-dvi' '.dwf' = 'drawing/x-dwf' @@ -182,7 +183,7 @@ Describe 'Get-PodeContentType' { '.etx' = 'text/x-setext' '.evy' = 'application/envoy' '.exe' = 'application/octet-stream' - '.exe.config' = 'text/xml' + '.exe.config' = 'application/xml' '.fdf' = 'application/vnd.fdf' '.fif' = 'application/fractals' '.filters' = 'application/xml' @@ -311,7 +312,7 @@ Describe 'Get-PodeContentType' { '.mka' = 'audio/x-matroska' '.mkv' = 'video/x-matroska' '.mmf' = 'application/x-smaf' - '.mno' = 'text/xml' + '.mno' = 'application/xml' '.mny' = 'application/x-msmoney' '.mod' = 'video/mpeg' '.mov' = 'video/quicktime' @@ -506,7 +507,7 @@ Describe 'Get-PodeContentType' { '.spx' = 'audio/ogg' '.src' = 'application/x-wais-source' '.srf' = 'text/plain' - '.ssisdeploymentmanifest' = 'text/xml' + '.ssisdeploymentmanifest' = 'application/xml' '.ssm' = 'application/streamingmedia' '.sst' = 'application/vnd.ms-pki.certstore' '.stl' = 'application/vnd.ms-pki.stl' @@ -558,22 +559,22 @@ Describe 'Get-PodeContentType' { '.vdp' = 'text/plain' '.vdproj' = 'text/plain' '.vdx' = 'application/vnd.ms-visio.viewer' - '.vml' = 'text/xml' + '.vml' = 'application/xml' '.vscontent' = 'application/xml' - '.vsct' = 'text/xml' + '.vsct' = 'application/xml' '.vsd' = 'application/vnd.visio' '.vsi' = 'application/ms-vsi' '.vsix' = 'application/vsix' - '.vsixlangpack' = 'text/xml' - '.vsixmanifest' = 'text/xml' + '.vsixlangpack' = 'application/xml' + '.vsixmanifest' = 'application/xml' '.vsmdi' = 'application/xml' '.vspscc' = 'text/plain' '.vss' = 'application/vnd.visio' '.vsscc' = 'text/plain' - '.vssettings' = 'text/xml' + '.vssettings' = 'application/xml' '.vssscc' = 'text/plain' '.vst' = 'application/vnd.visio' - '.vstemplate' = 'text/xml' + '.vstemplate' = 'application/xml' '.vsto' = 'application/x-ms-vsto' '.vsw' = 'application/vnd.visio' '.vsx' = 'application/vnd.visio' @@ -617,7 +618,7 @@ Describe 'Get-PodeContentType' { '.wrl' = 'x-world/x-vrml' '.wrz' = 'x-world/x-vrml' '.wsc' = 'text/scriptlet' - '.wsdl' = 'text/xml' + '.wsdl' = 'application/xml' '.wvx' = 'video/x-ms-wvx' '.x' = 'application/directx' '.xaf' = 'x-world/x-vrml' @@ -643,26 +644,26 @@ Describe 'Get-PodeContentType' { '.xltm' = 'application/vnd.ms-excel.template.macroEnabled.12' '.xltx' = 'application/vnd.openxmlformats-officedocument.spreadsheetml.template' '.xlw' = 'application/vnd.ms-excel' - '.xml' = 'text/xml' + '.xml' = 'application/xml' '.xmp' = 'application/octet-stream' '.xmta' = 'application/xml' '.xof' = 'x-world/x-vrml' '.xoml' = 'text/plain' '.xpm' = 'image/x-xpixmap' '.xps' = 'application/vnd.ms-xpsdocument' - '.xrm-ms' = 'text/xml' + '.xrm-ms' = 'application/xml' '.xsc' = 'application/xml' - '.xsd' = 'text/xml' - '.xsf' = 'text/xml' - '.xsl' = 'text/xml' - '.xslt' = 'text/xml' + '.xsd' = 'application/xml' + '.xsf' = 'application/xml' + '.xsl' = 'application/xml' + '.xslt' = 'application/xml' '.xsn' = 'application/octet-stream' '.xss' = 'application/xml' '.xspf' = 'application/xspf+xml' '.xtp' = 'application/octet-stream' '.xwd' = 'image/x-xwindowdump' - '.yaml' = 'application/x-yaml' - '.yml' = 'application/x-yaml' + '.yaml' = 'application/yaml' + '.yml' = 'application/yaml' '.z' = 'application/x-compress' '.zip' = 'application/zip' } } @@ -689,7 +690,7 @@ Describe 'Get-PodeContentType' { '.accdw' = 'application/msaccess.webapplication' '.accft' = 'application/msaccess.ftemplate' '.acx' = 'application/internet-property-stream' - '.addin' = 'text/xml' + '.addin' = 'application/xml' '.ade' = 'application/msaccess' '.adobebridge' = 'application/x-bridge-url' '.adp' = 'application/msaccess' @@ -773,10 +774,10 @@ Describe 'Get-PodeContentType' { '.dib' = 'image/bmp' '.dif' = 'video/x-dv' '.dir' = 'application/x-director' - '.disco' = 'text/xml' + '.disco' = 'application/xml' '.divx' = 'video/divx' '.dll' = 'application/x-msdownload' - '.dll.config' = 'text/xml' + '.dll.config' = 'application/xml' '.dlm' = 'text/dlm' '.doc' = 'application/msword' '.docm' = 'application/vnd.ms-word.document.macroEnabled.12' @@ -786,8 +787,8 @@ Describe 'Get-PodeContentType' { '.dotx' = 'application/vnd.openxmlformats-officedocument.wordprocessingml.template' '.dsp' = 'application/octet-stream' '.dsw' = 'text/plain' - '.dtd' = 'text/xml' - '.dtsconfig' = 'text/xml' + '.dtd' = 'application/xml' + '.dtsconfig' = 'application/xml' '.dv' = 'video/x-dv' '.dvi' = 'application/x-dvi' '.dwf' = 'drawing/x-dwf' @@ -803,7 +804,7 @@ Describe 'Get-PodeContentType' { '.etx' = 'text/x-setext' '.evy' = 'application/envoy' '.exe' = 'application/octet-stream' - '.exe.config' = 'text/xml' + '.exe.config' = 'application/xml' '.fdf' = 'application/vnd.fdf' '.fif' = 'application/fractals' '.filters' = 'application/xml' @@ -932,7 +933,7 @@ Describe 'Get-PodeContentType' { '.mka' = 'audio/x-matroska' '.mkv' = 'video/x-matroska' '.mmf' = 'application/x-smaf' - '.mno' = 'text/xml' + '.mno' = 'application/xml' '.mny' = 'application/x-msmoney' '.mod' = 'video/mpeg' '.mov' = 'video/quicktime' @@ -1127,7 +1128,7 @@ Describe 'Get-PodeContentType' { '.spx' = 'audio/ogg' '.src' = 'application/x-wais-source' '.srf' = 'text/plain' - '.ssisdeploymentmanifest' = 'text/xml' + '.ssisdeploymentmanifest' = 'application/xml' '.ssm' = 'application/streamingmedia' '.sst' = 'application/vnd.ms-pki.certstore' '.stl' = 'application/vnd.ms-pki.stl' @@ -1179,22 +1180,22 @@ Describe 'Get-PodeContentType' { '.vdp' = 'text/plain' '.vdproj' = 'text/plain' '.vdx' = 'application/vnd.ms-visio.viewer' - '.vml' = 'text/xml' + '.vml' = 'application/xml' '.vscontent' = 'application/xml' - '.vsct' = 'text/xml' + '.vsct' = 'application/xml' '.vsd' = 'application/vnd.visio' '.vsi' = 'application/ms-vsi' '.vsix' = 'application/vsix' - '.vsixlangpack' = 'text/xml' - '.vsixmanifest' = 'text/xml' + '.vsixlangpack' = 'application/xml' + '.vsixmanifest' = 'application/xml' '.vsmdi' = 'application/xml' '.vspscc' = 'text/plain' '.vss' = 'application/vnd.visio' '.vsscc' = 'text/plain' - '.vssettings' = 'text/xml' + '.vssettings' = 'application/xml' '.vssscc' = 'text/plain' '.vst' = 'application/vnd.visio' - '.vstemplate' = 'text/xml' + '.vstemplate' = 'application/xml' '.vsto' = 'application/x-ms-vsto' '.vsw' = 'application/vnd.visio' '.vsx' = 'application/vnd.visio' @@ -1238,7 +1239,7 @@ Describe 'Get-PodeContentType' { '.wrl' = 'x-world/x-vrml' '.wrz' = 'x-world/x-vrml' '.wsc' = 'text/scriptlet' - '.wsdl' = 'text/xml' + '.wsdl' = 'application/xml' '.wvx' = 'video/x-ms-wvx' '.x' = 'application/directx' '.xaf' = 'x-world/x-vrml' @@ -1264,30 +1265,30 @@ Describe 'Get-PodeContentType' { '.xltm' = 'application/vnd.ms-excel.template.macroEnabled.12' '.xltx' = 'application/vnd.openxmlformats-officedocument.spreadsheetml.template' '.xlw' = 'application/vnd.ms-excel' - '.xml' = 'text/xml' + '.xml' = 'application/xml' '.xmp' = 'application/octet-stream' '.xmta' = 'application/xml' '.xof' = 'x-world/x-vrml' '.xoml' = 'text/plain' '.xpm' = 'image/x-xpixmap' '.xps' = 'application/vnd.ms-xpsdocument' - '.xrm-ms' = 'text/xml' + '.xrm-ms' = 'application/xml' '.xsc' = 'application/xml' - '.xsd' = 'text/xml' - '.xsf' = 'text/xml' - '.xsl' = 'text/xml' - '.xslt' = 'text/xml' + '.xsd' = 'application/xml' + '.xsf' = 'application/xml' + '.xsl' = 'application/xml' + '.xslt' = 'application/xml' '.xsn' = 'application/octet-stream' '.xss' = 'application/xml' '.xspf' = 'application/xspf+xml' '.xtp' = 'application/octet-stream' '.xwd' = 'image/x-xwindowdump' - '.yaml' = 'application/x-yaml' - '.yml' = 'application/x-yaml' + '.yaml' = 'application/yaml' + '.yml' = 'application/yaml' '.z' = 'application/x-compress' '.zip' = 'application/zip' } } - It "Returns correct content type for <_>" -ForEach ($types.Keys) { + It 'Returns correct content type for <_>' -ForEach ($types.Keys) { Get-PodeContentType -Extension $_ | Should -Be $types[$_] } @@ -1456,9 +1457,9 @@ Describe 'Get-PodeStatusDescription' { '511' = 'Network Authentication Required' '526' = 'Invalid SSL Certificate' } } - It "Returns description for the <_> StatusCode" -ForEach ($codes.Keys) { + It 'Returns description for the <_> StatusCode' -ForEach ($codes.Keys) { Get-PodeStatusDescription -StatusCode $_ | Should -Be $codes[$_] } - }#> + } } \ No newline at end of file diff --git a/tests/unit/Metrics.Tests.ps1 b/tests/unit/Metrics.Tests.ps1 index 3430ac158..5dcbe9b05 100644 --- a/tests/unit/Metrics.Tests.ps1 +++ b/tests/unit/Metrics.Tests.ps1 @@ -4,6 +4,7 @@ BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' $PodeContext = @{ Metrics = @{ diff --git a/tests/unit/Middleware.Tests.ps1 b/tests/unit/Middleware.Tests.ps1 index f4b7034d9..62c5ff9ec 100644 --- a/tests/unit/Middleware.Tests.ps1 +++ b/tests/unit/Middleware.Tests.ps1 @@ -1,24 +1,26 @@ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] param() BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' } Describe 'Get-PodeInbuiltMiddleware' { Context 'Invalid parameters supplied' { It 'Throws null name parameter error' { - { Get-PodeInbuiltMiddleware -Name $null -ScriptBlock {} } | Should -Throw -ExpectedMessage '*null or empty*' + { Get-PodeInbuiltMiddleware -Name $null -ScriptBlock {} } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Get-PodeInbuiltMiddleware' } It 'Throws empty name parameter error' { - { Get-PodeInbuiltMiddleware -Name ([string]::Empty) -ScriptBlock {} } | Should -Throw -ExpectedMessage '*null or empty*' + { Get-PodeInbuiltMiddleware -Name ([string]::Empty) -ScriptBlock {} } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Get-PodeInbuiltMiddleware' } It 'Throws null logic error' { - { Get-PodeInbuiltMiddleware -Name 'test' -ScriptBlock $null } | Should -Throw -ExpectedMessage '*argument is null*' + { Get-PodeInbuiltMiddleware -Name 'test' -ScriptBlock $null } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Get-PodeInbuiltMiddleware' } } @@ -106,12 +108,14 @@ Describe 'Middleware' { $PodeContext = @{ 'Server' = @{ 'Middleware' = @(); }; } Add-PodeMiddleware -Name 'Test1' -ScriptBlock { write-host 'middle1' } - { Add-PodeMiddleware -Name 'Test1' -ScriptBlock { write-host 'middle2' } } | Should -Throw -ExpectedMessage '*already defined*' + $expectedMessage = ($PodeLocale.middlewareAlreadyDefinedExceptionMessage -f 'Test1').Replace('[','`[').Replace(']','`]') # -replace '\[', '`[' -replace '\]', '`]' + { Add-PodeMiddleware -Name 'Test1' -ScriptBlock { write-host 'middle2' } } | Should -Throw -ExpectedMessage $expectedMessage #'*already defined*' } It 'Throws error when adding middleware hash with no logic' { $PodeContext = @{ 'Server' = @{ 'Middleware' = @(); }; } - { Add-PodeMiddleware -Name 'Test1' -InputObject @{ 'Rand' = { write-host 'middle1' } } } | Should -Throw -ExpectedMessage '*no logic supplied*' + $expectedMessage = $PodeLocale.middlewareNoLogicSuppliedExceptionMessage.Replace('[','`[').Replace(']','`]') # -replace '\[', '`[' -replace '\]', '`]' + { Add-PodeMiddleware -Name 'Test1' -InputObject @{ 'Rand' = { write-host 'middle1' } } } | Should -Throw -ExpectedMessage $expectedMessage # '*no logic supplied*' } It 'Adds single middleware hash to list' { @@ -158,7 +162,8 @@ Describe 'Middleware' { $PodeContext = @{ 'Server' = @{ 'Middleware' = @(); }; } Add-PodeMiddleware -Name 'Test1' -InputObject @{ 'Logic' = { write-host 'middle1' } } - { Add-PodeMiddleware -Name 'Test1' -InputObject @{ 'Logic' = { write-host 'middle2' } } } | Should -Throw -ExpectedMessage '*already defined*' + $expectedMessage = ($PodeLocale.middlewareAlreadyDefinedExceptionMessage -f 'Test1').Replace('[','`[').Replace(']','`]') # -replace '\[', '`[' -replace '\]', '`]' + { Add-PodeMiddleware -Name 'Test1' -InputObject @{ 'Logic' = { write-host 'middle2' } } } | Should -Throw -ExpectedMessage $expectedMessage #'*already defined*' } } } @@ -901,12 +906,12 @@ Describe 'Add-PodeBodyParser' { It 'Fails because a script is already defined' { $PodeContext = @{ 'Server' = @{ 'BodyParsers' = @{} } } { Add-PodeBodyParser -ContentType 'text/xml' -ScriptBlock {} } | Should -Not -Throw - { Add-PodeBodyParser -ContentType 'text/xml' -ScriptBlock {} } | Should -Throw -ExpectedMessage '*already a body parser*' + { Add-PodeBodyParser -ContentType 'text/xml' -ScriptBlock {} } | Should -Throw -ExpectedMessage ($PodeLocale.bodyParserAlreadyDefinedForContentTypeExceptionMessage -f 'text/xml') # A body-parser is already defined for the {0} content-type. } It 'Fails on an invalid content-type' { $PodeContext = @{ 'Server' = @{ 'BodyParsers' = @{} } } - { Add-PodeBodyParser -ContentType 'text_xml' -ScriptBlock {} } | Should -Throw -ExpectedMessage "*Cannot validate argument on parameter 'ContentType'*" + { Add-PodeBodyParser -ContentType 'text_xml' -ScriptBlock {} } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Add-PodeBodyParser' } It 'Adds a script for a content-type' { @@ -919,28 +924,28 @@ Describe 'Add-PodeBodyParser' { Describe 'Remove-PodeBodyParser' { It 'Fails on an invalid content-type' { $PodeContext = @{ 'Server' = @{ 'BodyParsers' = @{} } } - { Remove-PodeBodyParser -ContentType 'text_xml' } | Should -Throw -ExpectedMessage "*Cannot validate argument on parameter 'ContentType'*" + { Remove-PodeBodyParser -ContentType 'text_xml' } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Remove-PodeBodyParser' } It 'Does nothing if no script set for content-type' { $PodeContext = @{ 'Server' = @{ 'BodyParsers' = @{ - 'text/xml' = {} + 'application/xml' = {} } } } - { Remove-PodeBodyParser -ContentType 'text/yaml' } | Should -Not -Throw - $PodeContext.Server.BodyParsers.ContainsKey('text/xml') | Should -Be $true + { Remove-PodeBodyParser -ContentType 'application/yaml' } | Should -Not -Throw + $PodeContext.Server.BodyParsers.ContainsKey('application/xml') | Should -Be $true } It 'Removes the script for the content-type' { $PodeContext = @{ 'Server' = @{ 'BodyParsers' = @{ - 'text/xml' = {} + 'teapplicationxt/xml' = {} } } } - { Remove-PodeBodyParser -ContentType 'text/xml' } | Should -Not -Throw - $PodeContext.Server.BodyParsers.ContainsKey('text/xml') | Should -Be $false + { Remove-PodeBodyParser -ContentType 'application/xml' } | Should -Not -Throw + $PodeContext.Server.BodyParsers.ContainsKey('application/xml') | Should -Be $false } } \ No newline at end of file diff --git a/tests/unit/NameGenerator.Tests.ps1 b/tests/unit/NameGenerator.Tests.ps1 index 140bf974c..85a78f316 100644 --- a/tests/unit/NameGenerator.Tests.ps1 +++ b/tests/unit/NameGenerator.Tests.ps1 @@ -2,6 +2,7 @@ BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' } Describe 'Get-PodeRandomName' { diff --git a/tests/unit/OpenApi.Tests.ps1 b/tests/unit/OpenApi.Tests.ps1 index a9ceced07..4f4a26bbf 100644 --- a/tests/unit/OpenApi.Tests.ps1 +++ b/tests/unit/OpenApi.Tests.ps1 @@ -5,6 +5,7 @@ BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' } Describe 'OpenApi' { @@ -1679,10 +1680,10 @@ Describe 'OpenApi' { $result.Count | Should -Be 3 $result.type | Should -Be 'object' $result.xml | Should -Not -BeNullOrEmpty - $result.xml | Should -BeOfType [hashtable] + $result.xml | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $result.xml.Count | Should -Be 1 $result.properties | Should -Not -BeNullOrEmpty - $result.properties | Should -BeOfType [hashtable] + $result.properties | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $result.properties.Count | Should -Be 2 $result.properties.name | Should -Not -BeNullOrEmpty $result.properties.name | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] @@ -1708,10 +1709,10 @@ Describe 'OpenApi' { $result.Count | Should -Be 3 $result.type | Should -Be 'object' $result.xml | Should -Not -BeNullOrEmpty - $result.xml | Should -BeOfType [hashtable] + $result.xml | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $result.xml.Count | Should -Be 1 $result.properties | Should -Not -BeNullOrEmpty - $result.properties | Should -BeOfType [hashtable] + $result.properties | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $result.properties.Count | Should -Be 2 $result.properties.name | Should -Not -BeNullOrEmpty $result.properties.name | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] @@ -1744,10 +1745,10 @@ Describe 'OpenApi' { $result.Count | Should -Be 3 $result.type | Should -Be 'object' $result.xml | Should -Not -BeNullOrEmpty - $result.xml | Should -BeOfType [hashtable] + $result.xml | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $result.xml.Count | Should -Be 1 $result.properties | Should -Not -BeNullOrEmpty - $result.properties | Should -BeOfType [hashtable] + $result.properties | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $result.properties.Count | Should -Be 2 $result.properties.name | Should -Not -BeNullOrEmpty $result.properties.name | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] @@ -1774,10 +1775,10 @@ Describe 'OpenApi' { $result.Count | Should -Be 3 $result.type | Should -Be 'object' $result.xml | Should -Not -BeNullOrEmpty - $result.xml | Should -BeOfType [hashtable] + $result.xml | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $result.xml.Count | Should -Be 1 $result.properties | Should -Not -BeNullOrEmpty - $result.properties | Should -BeOfType [hashtable] + $result.properties | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $result.properties.Count | Should -Be 2 $result.properties.name | Should -Not -BeNullOrEmpty $result.properties.name | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] @@ -2085,15 +2086,14 @@ Describe 'OpenApi' { { Merge-PodeOAProperty -Type AllOf -DiscriminatorProperty 'name' -ObjectDefinitions @('Pet', (New-PodeOAObjectProperty -Properties @((New-PodeOAIntProperty -Name 'id'), (New-PodeOAStringProperty -Name 'name'))) - ) } | Should -Throw -ExpectedMessage 'Discriminator parameter is not compatible with allOf' + # Discriminator parameter is not compatible with allOf + ) } | Should -Throw -ExpectedMessage $PodeLocale.discriminatorIncompatibleWithAllOfExceptionMessage } - #Should -Throw -ExpectedMessage 'Discriminator parameter is not compatible with allOf' - It 'AllOf and ObjectDefinitions not an object' { { Merge-PodeOAProperty -Type AllOf -DiscriminatorProperty 'name' -ObjectDefinitions @('Pet', ((New-PodeOAIntProperty -Name 'id'), (New-PodeOAStringProperty -Name 'name')) - ) } | Should -Throw -ExpectedMessage 'Only properties of type Object can be associated with allof' + ) } | Should -Throw -ExpectedMessage ($PodeLocale.propertiesTypeObjectAssociationExceptionMessage -f 'allOf') # Only properties of type Object can be associated with allOf } } @@ -2184,18 +2184,20 @@ Describe 'OpenApi' { - Context 'Set-PodeOARouteInfo' { + Context 'Set-PodeOARouteInfo single route' { BeforeEach { $Route = @{ OpenApi = @{ - Path = '/test' - Responses = @{ + Path = '/test' + Responses = @{ '200' = @{ description = 'OK' } 'default' = @{ description = 'Internal server error' } } - Parameters = $null - RequestBody = $null - Authentication = @() + Parameters = $null + RequestBody = $null + Authentication = @() + DefinitionTag = @('Default') + IsDefTagConfigured = $false } } @@ -2241,6 +2243,74 @@ Describe 'OpenApi' { } } + Context 'Set-PodeOARouteInfo multi routes' { + BeforeEach { + $Route = @(@{ + OpenApi = @{ + Path = '/test' + Responses = @{ + '200' = @{ description = 'OK' } + 'default' = @{ description = 'Internal server error' } + } + Parameters = $null + RequestBody = $null + Authentication = @() + DefinitionTag = @('Default') + } + }, + @{ + OpenApi = @{ + Path = '/test2' + Responses = @{ + '200' = @{ description = 'OK' } + 'default' = @{ description = 'Internal server error' } + } + Parameters = $null + RequestBody = $null + Authentication = @() + DefinitionTag = @('Default') + } + }) + + Add-PodeOATag -Name 'pet' -Description 'Everything about your Pets' -ExternalDoc (New-PodeOAExternalDoc -Description 'Find out more about Swagger' -Url 'http://swagger.io') + } + + It 'No switches' { + $Route | Set-PodeOARouteInfo -Summary 'Update an existing pet' -Description 'Update an existing pet by Id' -Tags 'pet' + $Route.OpenApi | Should -Not -BeNullOrEmpty + $Route.OpenApi.Summary | Should -Be @('Update an existing pet', 'Update an existing pet') + $Route.OpenApi.description | Should -Be @('Update an existing pet by Id', 'Update an existing pet by Id') + $Route.OpenApi.tags | Should -Be @('pet', 'pet') + $Route.OpenApi.swagger | Should -BeTrue + $Route.OpenApi.deprecated | Should -BeNullOrEmpty + } + It 'Deprecated' { + $Route | Set-PodeOARouteInfo -Summary 'Update an existing pet' -Description 'Update an existing pet by Id' -Tags 'pet' -Deprecated + $Route.OpenApi | Should -Not -BeNullOrEmpty + $Route.OpenApi.Summary | Should -Be @('Update an existing pet', 'Update an existing pet') + $Route.OpenApi.description | Should -Be @('Update an existing pet by Id', 'Update an existing pet by Id') + $Route.OpenApi.tags | Should -Be @('pet', 'pet') + $Route.OpenApi.swagger | Should -BeTrue + $Route.OpenApi.deprecated | Should -BeTrue + } + + It 'PassThru' { + $result = $Route | Set-PodeOARouteInfo -Summary 'Update an existing pet' -Description 'Update an existing pet by Id' -Tags 'pet' -PassThru + $result | Should -Not -BeNullOrEmpty + $result.OpenApi | Should -Not -BeNullOrEmpty + $Route.OpenApi.Summary | Should -Be @('Update an existing pet', 'Update an existing pet') + $Route.OpenApi.description | Should -Be @('Update an existing pet by Id', 'Update an existing pet by Id') + $Route.OpenApi.tags | Should -Be @('pet', 'pet') + $result.OpenApi.swagger | Should -BeTrue + $result.OpenApi.deprecated | Should -BeNullOrEmpty + } + + It 'PassThru with OperationID' { + { $Route | Set-PodeOARouteInfo -Summary 'Update an existing pet' -Description 'Update an existing pet by Id' -Tags 'pet' -OperationId 'updatePet' -PassThru } | + Should -Throw -ExpectedMessage ($PodeLocale.operationIdMustBeUniqueForArrayExceptionMessage -f 'updatePet') #'OperationID: {0} has to be unique and cannot be applied to an array.' + } + } + Context 'Add-PodeOAComponentParameter' { # Check if the function exists @@ -2276,7 +2346,7 @@ Describe 'OpenApi' { it 'throw error' { { Add-PodeOAComponentParameter -Parameter ( New-PodeOAIntProperty -Name 'petId' -Format Int64 -Description 'ID of the pet' | New-PodeOAObjectProperty ) } | - Should -Throw -ExpectedMessage 'The Parameter has no name. Please provide a name to this component using -Name property' + Should -Throw -ExpectedMessage $PodeLocale.parameterHasNoNameExceptionMessage # The Parameter has no name. Please give this component a name using the 'Name' parameter. } } Context 'ConvertTo-PodeOAParameter' { @@ -2408,7 +2478,8 @@ Describe 'OpenApi' { } It 'Path - ContentSchema - Exception -Required' { - { ConvertTo-PodeOAParameter -In Path -Description 'Feline description' -ContentType 'application/json' -Schema 'Cat' } | Should -Throw -ExpectedMessage '*the switch parameter `-Required*' + { ConvertTo-PodeOAParameter -In Path -Description 'Feline description' -ContentType 'application/json' -Schema 'Cat' } | + Should -Throw -ExpectedMessage $PodeLocale.pathParameterRequiresRequiredSwitchExceptionMessage # If the parameter location is 'Path', the switch parameter 'Required' is mandatory } } } @@ -2866,27 +2937,27 @@ Describe 'OpenApi' { } it 'default' { - Add-PodeOAComponentRequestBody -Name 'PetBodySchema' -Required -Description 'Pet in the store' -Content ( New-PodeOAContentMediaType -MediaType 'application/json' , 'application/xml', 'application/x-www-form-urlencoded' -Content 'Cat' ) + Add-PodeOAComponentRequestBody -Name 'PetBodySchema' -Required -Description 'Pet in the store' -Content ( New-PodeOAContentMediaType -ContentType 'application/json' , 'application/xml', 'application/x-www-form-urlencoded' -Content 'Cat' ) $result = $PodeContext.Server.OpenAPI.Definitions['default'].components.requestBodies['PetBodySchema'] $result | Should -Not -BeNullOrEmpty $result | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $result.Count | Should -Be 3 $result.description | Should -Be 'Pet in the store' - $result.content | Should -BeOfType [hashtable] + $result.content | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $result.content.Count | Should -Be 3 - $result.content.'application/json' | Should -BeOfType [hashtable] + $result.content.'application/json' | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $result.content.'application/json'.Count | Should -Be 1 - $result.content.'application/json'.schema | Should -BeOfType [hashtable] + $result.content.'application/json'.schema | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $result.content.'application/json'.schema.Count | Should -Be 1 $result.content.'application/json'.schema['$ref'] | Should -Be '#/components/schemas/Cat' - $result.content.'application/xml' | Should -BeOfType [hashtable] + $result.content.'application/xml' | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $result.content.'application/xml'.Count | Should -Be 1 - $result.content.'application/xml'.schema | Should -BeOfType [hashtable] + $result.content.'application/xml'.schema | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $result.content.'application/xml'.schema.Count | Should -Be 1 $result.content.'application/xml'.schema['$ref'] | Should -Be '#/components/schemas/Cat' - $result.content.'application/x-www-form-urlencoded' | Should -BeOfType [hashtable] + $result.content.'application/x-www-form-urlencoded' | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $result.content.'application/x-www-form-urlencoded'.Count | Should -Be 1 - $result.content.'application/x-www-form-urlencoded'.schema | Should -BeOfType [hashtable] + $result.content.'application/x-www-form-urlencoded'.schema | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $result.content.'application/x-www-form-urlencoded'.schema.Count | Should -Be 1 $result.content.'application/x-www-form-urlencoded'.schema['$ref'] | Should -Be '#/components/schemas/Cat' $result.required | Should -BeTrue @@ -2899,21 +2970,21 @@ Describe 'OpenApi' { $result | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $result.Count | Should -Be 3 $result.description | Should -Be 'Pet in the store' - $result.content | Should -BeOfType [hashtable] + $result.content | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $result.content.Count | Should -Be 3 - $result.content.'application/json' | Should -BeOfType [hashtable] + $result.content.'application/json' | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $result.content.'application/json'.Count | Should -Be 1 - $result.content.'application/json'.schema | Should -BeOfType [hashtable] + $result.content.'application/json'.schema | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $result.content.'application/json'.schema.Count | Should -Be 1 $result.content.'application/json'.schema['$ref'] | Should -Be '#/components/schemas/Cat' - $result.content.'application/xml' | Should -BeOfType [hashtable] + $result.content.'application/xml' | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $result.content.'application/xml'.Count | Should -Be 1 - $result.content.'application/xml'.schema | Should -BeOfType [hashtable] + $result.content.'application/xml'.schema | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $result.content.'application/xml'.schema.Count | Should -Be 1 $result.content.'application/xml'.schema['$ref'] | Should -Be '#/components/schemas/Cat' - $result.content.'application/x-www-form-urlencoded' | Should -BeOfType [hashtable] + $result.content.'application/x-www-form-urlencoded' | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $result.content.'application/x-www-form-urlencoded'.Count | Should -Be 1 - $result.content.'application/x-www-form-urlencoded'.schema | Should -BeOfType [hashtable] + $result.content.'application/x-www-form-urlencoded'.schema | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $result.content.'application/x-www-form-urlencoded'.schema.Count | Should -Be 1 $result.content.'application/x-www-form-urlencoded'.schema['$ref'] | Should -Be '#/components/schemas/Cat' $result.required | Should -BeTrue @@ -2962,13 +3033,13 @@ Describe 'OpenApi' { # Test return type It 'Returns an OrderedHashtable' { - $example = New-PodeOAExample -MediaType 'application/json' -Name 'user' -Summary 'JSON Example' -ExternalValue 'http://external.com' + $example = New-PodeOAExample -ContentType 'application/json' -Name 'user' -Summary 'JSON Example' -ExternalValue 'http://external.com' $example | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] } # Test output for a single MediaType It 'Correctly creates example for a single MediaType' { - $example = New-PodeOAExample -MediaType 'application/json' -Name 'user' -Summary 'JSON Example' -ExternalValue 'http://external.com' + $example = New-PodeOAExample -ContentType 'application/json' -Name 'user' -Summary 'JSON Example' -ExternalValue 'http://external.com' $example['application/json'].Keys -Contains 'user' | Should -Be $true $example['application/json']['user'].summary -eq 'JSON Example' | Should -Be $true $example['application/json']['user'].externalValue -eq 'http://external.com' | Should -Be $true @@ -2976,8 +3047,8 @@ Describe 'OpenApi' { # Test merging behavior It 'Correctly merges examples for multiple MediaTypes' { - $result = New-PodeOAExample -MediaType 'application/json' -Name 'user' -Summary 'JSON Example' -Value '[]' | - New-PodeOAExample -MediaType 'application/xml' -Name 'user' -Summary 'XML Example' -Value '<>' + $result = New-PodeOAExample -ContentType 'application/json' -Name 'user' -Summary 'JSON Example' -Value '[]' | + New-PodeOAExample -ContentType 'application/xml' -Name 'user' -Summary 'XML Example' -Value '<>' $result.Count -eq 2 | Should -Be $true $result['application/json']['user'].summary -eq 'JSON Example' | Should -Be $true @@ -3034,6 +3105,139 @@ Describe 'OpenApi' { } } + Describe 'Rename-PodeOADefinitionTag' { + # Mocking the PodeContext to simulate the environment + BeforeEach { + $PodeContext = @{ + Server = @{ + OpenAPI = @{ + Definitions = @{ + 'oldTag' = @{ + # Mock definition details + Description = 'Old tag description' + } + } + SelectedDefinitionTag = 'oldTag' + DefinitionTagSelectionStack = [System.Collections.Stack]@() + } + Web = @{ + OpenApi = @{ + DefaultDefinitionTag = 'oldTag' + } + } + } + } + } + + # Test case: Renaming a specific tag + It 'Renames a specific OpenAPI definition tag' { + Rename-PodeOADefinitionTag -Tag 'oldTag' -NewTag 'newTag' + + # Check if the new tag exists + $PodeContext.Server.OpenAPI.Definitions.ContainsKey('newTag') | Should -BeTrue + # Check if the old tag is removed + $PodeContext.Server.OpenAPI.Definitions.ContainsKey('oldTag') | Should -BeFalse + # Check if the selected definition tag is updated + $PodeContext.Server.OpenAPI.SelectedDefinitionTag | Should -Be 'newTag' + } + + # Test case: Renaming the default tag + It 'Renames the default OpenAPI definition tag when Tag parameter is not specified' { + Rename-PodeOADefinitionTag -NewTag 'newDefaultTag' + + # Check if the new default tag is set + $PodeContext.Server.Web.OpenApi.DefaultDefinitionTag | Should -Be 'newDefaultTag' + # Check if the new tag exists + $PodeContext.Server.OpenAPI.Definitions.ContainsKey('newDefaultTag') | Should -BeTrue + # Check if the old tag is removed + $PodeContext.Server.OpenAPI.Definitions.ContainsKey('oldTag') | Should -BeFalse + } + + # Test case: Error when new tag already exists + It 'Throws an error when the new tag name already exists' { + $PodeContext.Server.OpenAPI.Definitions['existingTag'] = @{ + # Mock definition details + Description = 'Existing tag description' + } + + { Rename-PodeOADefinitionTag -Tag 'oldTag' -NewTag 'existingTag' } | Should -Throw -ExpectedMessage ($PodeLocale.openApiDefinitionAlreadyExistsExceptionMessage -f 'existingTag') + } + + # Test case: Error when used inside Select-PodeOADefinition ScriptBlock + It 'Throws an error when used inside a Select-PodeOADefinition ScriptBlock' { + $PodeContext.Server.OpenApi.DefinitionTagSelectionStack.Push('dummy') + + { Rename-PodeOADefinitionTag -Tag 'oldTag' -NewTag 'newTag' } | Should -Throw -ExpectedMessage ($PodeLocale.renamePodeOADefinitionTagExceptionMessage) + + # Clear the stack after test + $PodeContext.Server.OpenApi.DefinitionTagSelectionStack.Clear() + } + } + + + Describe 'Set-PodeOARequest' { + + It 'Sets Parameters on the route if provided' { + $route = @{ + Method = 'GET' + OpenApi = @{} + } + $parameters = @( + @{ Name = 'param1'; In = 'query' } + ) + + Set-PodeOARequest -Route $route -Parameters $parameters + + $route.OpenApi.Parameters | Should -BeExactly $parameters + } + + It 'Sets RequestBody on the route if method is POST' { + $route = @{ + Method = 'POST' + OpenApi = @{} + } + $requestBody = @{ Content = 'application/json' } + + Set-PodeOARequest -Route $route -RequestBody $requestBody + + $route.OpenApi.RequestBody | Should -BeExactly $requestBody + } + + It 'Throws an exception if RequestBody is set on a method that does not allow it' { + $route = @{ + Method = 'GET' + OpenApi = @{} + } + $requestBody = @{ Content = 'application/json' } + + { + Set-PodeOARequest -Route $route -RequestBody $requestBody + } | Should -Throw -ExpectedMessage ($PodeLocale.getRequestBodyNotAllowedExceptionMessage -f 'GET') + } + + It 'Returns the route when PassThru is used' { + $route = @{ + Method = 'POST' + OpenApi = @{} + } + + $result = Set-PodeOARequest -Route $route -PassThru + + $result | Should -BeExactly $route + } + + It 'Does not set RequestBody if not provided' { + $route = @{ + Method = 'PUT' + OpenApi = @{} + } + + Set-PodeOARequest -Route $route + + $route.OpenApi.RequestBody | Should -BeNullOrEmpty + } + } + Context 'Pet Object example' { @@ -3062,7 +3266,7 @@ Describe 'OpenApi' { (New-PodeOAStringProperty -Name 'status' -Description 'pet status in the store' -Enum @('available', 'pending', 'sold')) ) $Pet.type | Should -be 'object' - $Pet.xml | Should -BeOfType [hashtable] + $Pet.xml | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $Pet.xml.Count | Should -Be 1 $Pet.xml.name | Should -Be 'pet' $Pet.name | Should -Be 'Pet' @@ -3117,7 +3321,7 @@ Describe 'OpenApi' { New-PodeOAStringProperty -Name 'status' -Description 'pet status in the store' -Enum @('available', 'pending', 'sold') | New-PodeOAObjectProperty -Name 'Pet' -XmlName 'pet' $Pet.type | Should -be 'object' - $Pet.xml | Should -BeOfType [hashtable] + $Pet.xml | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] $Pet.xml.Count | Should -Be 1 $Pet.xml.name | Should -Be 'pet' $Pet.name | Should -Be 'Pet' @@ -3158,4 +3362,4 @@ Describe 'OpenApi' { } } -} +} \ No newline at end of file diff --git a/tests/unit/PrivateOpenApi.Tests.ps1 b/tests/unit/PrivateOpenApi.Tests.ps1 index c9b1c4a89..4a6b807fe 100644 --- a/tests/unit/PrivateOpenApi.Tests.ps1 +++ b/tests/unit/PrivateOpenApi.Tests.ps1 @@ -5,6 +5,7 @@ BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' } Describe 'PrivateOpenApi' { @@ -13,11 +14,11 @@ Describe 'PrivateOpenApi' { function GetPodeContext { return @{ Server = @{ - Security = @{ + Security = @{ autoheaders = $false } - Authentications=@{} - OpenAPI = @{ + Authentications = @{} + OpenAPI = @{ SelectedDefinitionTag = 'default' Definitions = @{ default = Get-PodeOABaseObject @@ -287,32 +288,32 @@ Describe 'PrivateOpenApi' { } - Describe "Set-PodeOAAuth Tests" { + Describe 'Set-PodeOAAuth Tests' { BeforeAll { # Mock Test-PodeAuthExists to simulate authentication method existence Mock Test-PodeAuthExists { return $true } } - It "Applies multiple authentication methods to a route" { + It 'Applies multiple authentication methods to a route' { $route = @{ OpenApi = @{} } { Set-PodeOAAuth -Route @($route) -Name @('BasicAuth', 'ApiKeyAuth') } | Should -Not -Throw $route.OpenApi.Authentication.Count | Should -Be 2 } - It "Throws an exception for non-existent authentication method" { + It 'Throws an exception for non-existent authentication method' { Mock Test-PodeAuthExists { return $false } $route = @{ OpenApi = @{} } { Set-PodeOAAuth -Route @($route) -Name 'InvalidAuth' } | Should -Throw } - It "Allows anonymous access" { + It 'Allows anonymous access' { $route = @{ OpenApi = @{ Authentication = @{} } } { Set-PodeOAAuth -Route @($route) -Name 'BasicAuth' -AllowAnon } | Should -Not -Throw $route.OpenApi.Authentication.keys -contains '%_allowanon_%' | Should -Be $true $route.OpenApi.Authentication['%_allowanon_%'] | Should -BeNullOrEmpty } - It "Applies both authenticated and anonymous access to a route" { + It 'Applies both authenticated and anonymous access to a route' { $route = @{ OpenApi = @{} } { Set-PodeOAAuth -Route @($route) -Name @('BasicAuth') -AllowAnon } | Should -Not -Throw $route.OpenApi.Authentication.Count | Should -Be 2 @@ -321,8 +322,8 @@ Describe 'PrivateOpenApi' { } } - Describe "Get-PodeOABaseObject Tests" { - It "Returns the correct base OpenAPI object structure" { + Describe 'Get-PodeOABaseObject Tests' { + It 'Returns the correct base OpenAPI object structure' { $baseObject = Get-PodeOABaseObject $baseObject | Should -BeOfType [hashtable] @@ -335,30 +336,27 @@ Describe 'PrivateOpenApi' { } } - Describe "Initialize-PodeOpenApiTable Tests" { - It "Initializes OpenAPI table with default settings" { + Describe 'Initialize-PodeOpenApiTable Tests' { + It 'Initializes OpenAPI table with default settings' { $openApiTable = Initialize-PodeOpenApiTable $openApiTable | Should -BeOfType [hashtable] - $openApiTable.DefinitionTagSelectionStack -is [System.Collections.Generic.Stack[System.Object]] | Should -BeTrue - $openApiTable.DefaultDefinitionTag | Should -Be "default" - $openApiTable.SelectedDefinitionTag | Should -Be "default" + $openApiTable.DefinitionTagSelectionStack -is [System.Collections.Generic.Stack[System.Object]] | Should -BeTrue + $openApiTable.SelectedDefinitionTag | Should -Be 'default' $openApiTable.Definitions | Should -BeOfType [hashtable] - $openApiTable.Definitions["default"] | Should -BeOfType [hashtable] + $openApiTable.Definitions['default'] | Should -BeOfType [hashtable] } - It "Initializes OpenAPI table with custom definition tag" { - $customTag = "api-v1" + It 'Initializes OpenAPI table with custom definition tag' { + $customTag = 'api-v1' $openApiTable = Initialize-PodeOpenApiTable -DefaultDefinitionTag $customTag - - $openApiTable.DefaultDefinitionTag | Should -Be $customTag $openApiTable.SelectedDefinitionTag | Should -Be $customTag $openApiTable.Definitions | Should -BeOfType [hashtable] $openApiTable.Definitions[$customTag] | Should -BeOfType [hashtable] } } - Describe "ConvertTo-PodeOASchemaObjectProperty Tests" { + Describe 'ConvertTo-PodeOASchemaObjectProperty Tests' { BeforeAll { # Mock ConvertTo-PodeOASchemaProperty to simulate its behavior Mock ConvertTo-PodeOASchemaProperty { return @{ type = $_.type; processed = $true } } @@ -366,57 +364,57 @@ Describe 'PrivateOpenApi' { It "Converts a list of properties excluding 'allOf', 'oneOf', 'anyOf'" { $properties = @( - @{ name = "prop1"; type = "string" }, - @{ name = "prop2"; type = "integer" }, - @{ name = "prop3"; type = "allOf" } + @{ name = 'prop1'; type = 'string' }, + @{ name = 'prop2'; type = 'integer' }, + @{ name = 'prop3'; type = 'allOf' } ) - $result = ConvertTo-PodeOASchemaObjectProperty -Properties $properties -DefinitionTag "myTag" + $result = ConvertTo-PodeOASchemaObjectProperty -Properties $properties -DefinitionTag 'myTag' $result.Count | Should -Be 2 - $result["prop1"].processed | Should -Be $true - $result["prop2"].processed | Should -Be $true - $result.ContainsKey("prop3") | Should -Be $false + $result['prop1'].processed | Should -Be $true + $result['prop2'].processed | Should -Be $true + $result.keys -contains 'prop3' | Should -Be $false } - It "Forms valid schema object for non-excluded properties" { + It 'Forms valid schema object for non-excluded properties' { $properties = @( - @{ name = "prop1"; type = "string" } + @{ name = 'prop1'; type = 'string' } ) - $result = ConvertTo-PodeOASchemaObjectProperty -Properties $properties -DefinitionTag "myTag" + $result = ConvertTo-PodeOASchemaObjectProperty -Properties $properties -DefinitionTag 'myTag' $result.Count | Should -Be 1 - $result["prop1"].type | Should -Be "string" - $result["prop1"].processed | Should -Be $true + $result['prop1'].type | Should -Be 'string' + $result['prop1'].processed | Should -Be $true } } - Describe "ConvertTo-PodeOAObjectSchema Tests" { + Describe 'ConvertTo-PodeOAObjectSchema Tests' { Mock ConvertTo-PodeOASchemaProperty { return @{ mockedResult = $true } } Mock Test-PodeOAComponentInternal { return $true } Mock Test-PodeOAVersion { param($Version) $Version -le 3.0 } - It "Converts valid content to schema object" { + It 'Converts valid content to schema object' { $content = @{ - "application/json" = @{ type = "String" } + 'application/json' = @{ type = 'String' } } - $result = ConvertTo-PodeOAObjectSchema -Content $content -DefinitionTag "myTag" + $result = ConvertTo-PodeOAObjectSchema -Content $content -DefinitionTag 'myTag' - $result.ContainsKey("application/json") | Should -Be $true - $result["application/json"].schema.type | Should -Be 'string' + $result.Keys -contains 'application/json' | Should -Be $true + $result['application/json'].schema.type | Should -Be 'string' } - It "Handles array structures correctly" { + It 'Handles array structures correctly' { $content = @{ - "application/json" = @{ - __array = $true - __content = @{ type = "String" } + 'application/json' = @{ + __array = $true + __content = @{ type = 'String' } } } - $result = ConvertTo-PodeOAObjectSchema -Content $content -DefinitionTag "myTag" + $result = ConvertTo-PodeOAObjectSchema -Content $content -DefinitionTag 'myTag' - $result["application/json"].schema.type | Should -Be "array" - $result["application/json"].schema.Items.type | Should -Be 'string' + $result['application/json'].schema.type | Should -Be 'array' + $result['application/json'].schema.Items.type | Should -Be 'string' } } diff --git a/tests/unit/Responses.Tests.ps1 b/tests/unit/Responses.Tests.ps1 index 00e02dea4..80a10c741 100644 --- a/tests/unit/Responses.Tests.ps1 +++ b/tests/unit/Responses.Tests.ps1 @@ -5,6 +5,7 @@ BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' } @@ -239,6 +240,41 @@ Describe 'Write-PodeCsvResponse' { $r.ContentType | Should -Be $_ContentType } + It 'Converts and returns a value from a array of hashtable' { + $r = Write-PodeCsvResponse -Value @(@{ Name = 'Rick' }, @{ Name = 'Don' }) + $r.Value | Should -Be "`"Name`"$([environment]::NewLine)`"Rick`"$([environment]::NewLine)`"Don`"" + $r.ContentType | Should -Be $_ContentType + } + + It 'Converts and returns a value from a array of hashtable by Pipe' { + $r = @(@{ Name = 'Rick' }, @{ Name = 'Don' }) | Write-PodeCsvResponse + $r.Value | Should -Be "`"Name`"$([environment]::NewLine)`"Rick`"$([environment]::NewLine)`"Don`"" + $r.ContentType | Should -Be $_ContentType + } + + It 'Converts and returns a value from a array of PSCustomObject' { + $users = @([PSCustomObject]@{ + Name = 'Rick' + }, [PSCustomObject]@{ + Name = 'Don' + } + ) + $r = Write-PodeCsvResponse -Value $users + $r.Value | Should -Be "`"Name`"$([environment]::NewLine)`"Rick`"$([environment]::NewLine)`"Don`"" + $r.ContentType | Should -Be $_ContentType + } + + It 'Converts and returns a value from a array of PSCustomObject by Pipe' { + $r = @([PSCustomObject]@{ + Name = 'Rick' + }, [PSCustomObject]@{ + Name = 'Don' + } + ) | Write-PodeCsvResponse + $r.Value | Should -Be "`"Name`"$([environment]::NewLine)`"Rick`"$([environment]::NewLine)`"Don`"" + $r.ContentType | Should -Be $_ContentType + } + It 'Does nothing for an invalid file path' { Mock Test-PodePath { return $false } Write-PodeCsvResponse -Path 'fake-file' | Out-Null @@ -259,7 +295,7 @@ Describe 'Write-PodeCsvResponse' { Describe 'Write-PodeXmlResponse' { BeforeEach { Mock Write-PodeTextResponse { return @{ 'Value' = $Value; 'ContentType' = $ContentType; } } - $_ContentType = 'text/xml' + $_ContentType = 'application/xml' } It 'Returns an empty value for an empty value' { $r = Write-PodeXmlResponse -Value ([string]::Empty) @@ -279,6 +315,42 @@ Describe 'Write-PodeXmlResponse' { $r.ContentType | Should -Be $_ContentType } + It 'Converts and returns a value from a array of hashtable by pipe' { + $r = @(@{ Name = 'Rick' }, @{ Name = 'Don' }) | Write-PodeXmlResponse + ($r.Value -ireplace '[\r\n ]', '') | Should -Be 'RickDon' + $r.ContentType | Should -Be $_ContentType + } + + It 'Converts and returns a value from a array of hashtable' { + $r = Write-PodeXmlResponse -Value @(@{ Name = 'Rick' }, @{ Name = 'Don' }) + ($r.Value -ireplace '[\r\n ]', '') | Should -Be 'RickDon' + $r.ContentType | Should -Be $_ContentType + } + + It 'Converts and returns a value from a array of PSCustomObject' { + $users = @([PSCustomObject]@{ + Name = 'Rick' + }, [PSCustomObject]@{ + Name = 'Don' + } + ) + $r = Write-PodeXmlResponse -Value $users + ($r.Value -ireplace '[\r\n ]', '') | Should -Be 'RickDon' + $r.ContentType | Should -Be $_ContentType + } + + It 'Converts and returns a value from a array of PSCustomObject passed by pipe' { + $r = @([PSCustomObject]@{ + Name = 'Rick' + }, [PSCustomObject]@{ + Name = 'Don' + } + ) | Write-PodeXmlResponse + ($r.Value -ireplace '[\r\n ]', '') | Should -Be 'RickDon' + $r.ContentType | Should -Be $_ContentType + } + + It 'Does nothing for an invalid file path' { Mock Test-PodePath { return $false } Write-PodeXmlResponse -Path 'fake-file' | Out-Null @@ -397,7 +469,7 @@ Describe 'Use-PodePartialView' { It 'Throws an error for a path that does not exist' { Mock Test-PodePath { return $false } - { Use-PodePartialView -Path 'sub-view.pode' } | Should -Throw -ExpectedMessage '*File not found*' + { Use-PodePartialView -Path 'sub-view.pode' } | Should -Throw -ExpectedMessage ($PodeLocale.viewsPathDoesNotExistExceptionMessage -f '*' ) # The Views path does not exist: sub-view.pode' } diff --git a/tests/unit/Routes.Tests.ps1 b/tests/unit/Routes.Tests.ps1 index 88025a2f7..b5f746448 100644 --- a/tests/unit/Routes.Tests.ps1 +++ b/tests/unit/Routes.Tests.ps1 @@ -1,25 +1,27 @@ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] param() BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' $PodeContext = @{ 'Server' = $null; } } Describe 'Find-PodeRoute' { Context 'Invalid parameters supplied' { It 'Throw invalid method error for no method' { - { Find-PodeRoute -Method 'MOO' -Path '/' } | Should -Throw -ExpectedMessage "*Cannot validate argument on parameter 'Method'*" + { Find-PodeRoute -Method 'MOO' -Path '/' } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Find-PodeRoute' } It 'Throw null route parameter error' { - { Find-PodeRoute -Method GET -Path $null } | Should -Throw -ExpectedMessage '*The argument is null or empty*' + { Find-PodeRoute -Method GET -Path $null } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Find-PodeRoute' } It 'Throw empty route parameter error' { - { Find-PodeRoute -Method GET -Path ([string]::Empty) } | Should -Throw -ExpectedMessage '*The argument is null or empty*' + { Find-PodeRoute -Method GET -Path ([string]::Empty) } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Find-PodeRoute' } } @@ -102,7 +104,7 @@ Describe 'Add-PodeStaticRoute' { It 'Throws error when adding static route for non-existing folder' { Mock Test-PodePath { return $false } $PodeContext.Server = @{ 'Routes' = @{ 'STATIC' = @{}; }; 'Root' = $pwd; FindEndpoints = @{} } - { Add-PodeStaticRoute -Path '/assets' -Source './assets' } | Should -Throw -ExpectedMessage '*does not exist*' + { Add-PodeStaticRoute -Path '/assets' -Source './assets' } | Should -Throw -ExpectedMessage ($PodeLocale.sourcePathDoesNotExistForStaticRouteExceptionMessage -f '*', '*/assets' ) #'*does not exist*' } } @@ -151,6 +153,42 @@ Describe 'Remove-PodeRoute' { $routes.ContainsKey('/users') | Should -Be $true $routes['/users'].Length | Should -Be 1 } + + It 'Removes a route and cleans up OpenAPI operationId' { + Add-PodeRoute -PassThru -Method Get -Path '/users' -ScriptBlock { Write-Host 'hello' } | Set-PodeOARouteInfo -Summary 'Test user' -OperationId 'getUsers' + + $routes = $PodeContext.Server.Routes['GET'] + $routes | Should -Not -Be $null + $routes.ContainsKey('/users') | Should -Be $true + $routes['/users'].Length | Should -Be 1 + + Remove-PodeRoute -Method Get -Path '/users' + + $routes = $PodeContext.Server.Routes['GET'] + $routes | Should -Not -Be $null + $routes.ContainsKey('/users') | Should -Be $false + $PodeContext.Server.OpenAPI.Definitions.default.hiddenComponents.operationId | Should -Not -Contain 'getUsers' + } + + It 'Adds two routes and removes on route and cleans up OpenAPI operationId' { + Add-PodeEndpoint -Address '127.0.0.1' -Port 8080 -Protocol Http -Name user + + Add-PodeRoute -PassThru -Method Get -Path '/users' -ScriptBlock { Write-Host 'hello' } | Set-PodeOARouteInfo -Summary 'Test user' -OperationId 'getUsers' + Add-PodeRoute -PassThru -Method Get -Path '/users' -EndpointName user -ScriptBlock { Write-Host 'hello' } | Set-PodeOARouteInfo -Summary 'Test user2' -OperationId 'getUsers2' + + $routes = $PodeContext.Server.Routes['GET'] + $routes | Should -Not -Be $null + $routes.ContainsKey('/users') | Should -Be $true + $routes['/users'].Length | Should -Be 2 + + Remove-PodeRoute -Method Get -Path '/users' -EndpointName 'user' + + $routes = $PodeContext.Server.Routes['GET'] + $routes | Should -Not -Be $null + $routes.ContainsKey('/users') | Should -Be $true + $routes['/users'].Length | Should -Be 1 + $PodeContext.Server.OpenAPI.Definitions.default.hiddenComponents.operationId | Should -Not -Contain 'getUsers2' + } } Describe 'Remove-PodeStaticRoute' { @@ -259,39 +297,44 @@ Describe 'Add-PodeRoute' { } } It 'Throws invalid method error for no method' { - { Add-PodeRoute -Method 'MOO' -Path '/' -ScriptBlock {} } | Should -Throw -ExpectedMessage "*Cannot validate argument on parameter 'Method'*" + { Add-PodeRoute -Method 'MOO' -Path '/' -ScriptBlock {} } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Add-PodeRoute' } It 'Throws null route parameter error' { - { Add-PodeRoute -Method GET -Path $null -ScriptBlock {} } | Should -Throw -ExpectedMessage '*it is an empty string*' + { Add-PodeRoute -Method GET -Path $null -ScriptBlock {} } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorEmptyStringNotAllowed,Add-PodeRoute'#-ExpectedMessage } It 'Throws empty route parameter error' { - { Add-PodeRoute -Method GET -Path ([string]::Empty) -ScriptBlock {} } | Should -Throw -ExpectedMessage '*it is an empty string*' + { Add-PodeRoute -Method GET -Path ([string]::Empty) -ScriptBlock {} } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorEmptyStringNotAllowed,Add-PodeRoute' } It 'Throws error when scriptblock and file path supplied' { - { Add-PodeRoute -Method GET -Path '/' -ScriptBlock { write-host 'hi' } -FilePath './path' } | Should -Throw -ExpectedMessage '*parameter set cannot be resolved*' + { Add-PodeRoute -Method GET -Path '/' -ScriptBlock { write-host 'hi' } -FilePath './path' } | Should -Throw -ErrorId 'AmbiguousParameterSet,Add-PodeRoute' } It 'Throws error when file path is a directory' { Mock Get-PodeRelativePath { return $Path } Mock Test-PodePath { return $true } - { Add-PodeRoute -Method GET -Path '/' -FilePath './path' } | Should -Throw -ExpectedMessage '*cannot be a wildcard or a directory*' + # cannot be a wildcard or a directory + { Add-PodeRoute -Method GET -Path '/' -FilePath './path' } | Should -Throw -ExpectedMessage ($PodeLocale.invalidPathWildcardOrDirectoryExceptionMessage -f './path') } It 'Throws error when file path is a wildcard' { Mock Get-PodeRelativePath { return $Path } Mock Test-PodePath { return $true } - { Add-PodeRoute -Method GET -Path '/' -FilePath './path/*' } | Should -Throw -ExpectedMessage '*cannot be a wildcard or a directory*' + { Add-PodeRoute -Method GET -Path '/' -FilePath './path/*' } | Should -Throw -ExpectedMessage ($PodeLocale.invalidPathWildcardOrDirectoryExceptionMessage -f './path/*') #'*cannot be a wildcard or a directory*' } It 'Throws error because no scriptblock supplied' { - { Add-PodeRoute -Method GET -Path '/' -ScriptBlock {} } | Should -Throw -ExpectedMessage '*No logic passed*' + + # ?*[] can be escaped using backtick, ex `*. + $expectedMessage = ($PodeLocale.noLogicPassedForMethodRouteExceptionMessage -f 'GET', '/').Replace('[','`[').Replace(']','`]') + { Add-PodeRoute -Method GET -Path '/' -ScriptBlock {} } | Should -Throw -ExpectedMessage $expectedMessage # '*No logic passed*' + # -Throw -ExpectedMessage $expectedMessage # '*No logic passed*' } It 'Throws error because only querystring has been given' { - { Add-PodeRoute -Method GET -Path '?k=v' -ScriptBlock { write-host 'hi' } } | Should -Throw -ExpectedMessage '*No path supplied*' + { Add-PodeRoute -Method GET -Path '?k=v' -ScriptBlock { write-host 'hi' } } | Should -Throw -ExpectedMessage $PodeLocale.noPathSuppliedForRouteExceptionMessage #'*No path supplied*' } It 'Throws error because route already exists' { @@ -300,12 +343,12 @@ Describe 'Add-PodeRoute' { ) } } - - { Add-PodeRoute -Method GET -Path '/' -ScriptBlock { write-host 'hi' } } | Should -Throw -ExpectedMessage '*already defined*' + $expectedMessage = ($PodeLocale.methodPathAlreadyDefinedExceptionMessage -f 'GET', '/').Replace('[','`[').Replace(']','`]') + { Add-PodeRoute -Method GET -Path '/' -ScriptBlock { write-host 'hi' } } | Should -Throw -ExpectedMessage $expectedMessage #'*already defined*' } It 'Throws error on GET route for endpoint name not existing' { - { Add-PodeRoute -Method GET -Path '/users' -ScriptBlock { Write-Host 'hello' } -EndpointName 'test' } | Should -Throw -ExpectedMessage '*does not exist*' + { Add-PodeRoute -Method GET -Path '/users' -ScriptBlock { Write-Host 'hello' } -EndpointName 'test' } | Should -Throw -ExpectedMessage ($PodeLocale.endpointNameNotExistExceptionMessage -f 'Test') #*does not exist*' } It 'Adds route with simple url' { @@ -406,15 +449,15 @@ Describe 'Add-PodeRoute' { } It 'Adds route with middleware supplied as hashtable with null logic' { - { Add-PodeRoute -Method GET -Path '/users' -Middleware (@{ 'Logic' = $null }) -ScriptBlock {} } | Should -Throw -ExpectedMessage '*no logic defined*' + { Add-PodeRoute -Method GET -Path '/users' -Middleware (@{ 'Logic' = $null }) -ScriptBlock {} } | Should -Throw -ExpectedMessage $PodeLocale.hashtableMiddlewareNoLogicExceptionMessage #'*no logic defined*' } It 'Adds route with middleware supplied as hashtable with invalid type logic' { - { Add-PodeRoute -Method GET -Path '/users' -Middleware (@{ 'Logic' = 74 }) -ScriptBlock {} } | Should -Throw -ExpectedMessage '*invalid logic type*' + { Add-PodeRoute -Method GET -Path '/users' -Middleware (@{ 'Logic' = 74 }) -ScriptBlock {} } | Should -Throw -ExpectedMessage ($PodeLocale.invalidLogicTypeInHashtableMiddlewareExceptionMessage -f 'Int32') #'*invalid logic type*' } It 'Adds route with invalid middleware type' { - { Add-PodeRoute -Method GET -Path '/users' -Middleware 74 -ScriptBlock {} } | Should -Throw -ExpectedMessage '*invalid type*' + { Add-PodeRoute -Method GET -Path '/users' -Middleware 74 -ScriptBlock {} } | Should -Throw -ExpectedMessage ($PodeLocale.invalidMiddlewareTypeExceptionMessage -f 'Int32') #*invalid type*' } It 'Adds route with middleware supplied as hashtable and empty logic' { @@ -580,11 +623,11 @@ Describe 'ConvertTo-PodeRoute' { Mock Get-Module { return @{ ExportedCommands = @{ Keys = @('Some-ModuleCommand1', 'Some-ModuleCommand2') } } } } It 'Throws error when module does not contain command' { - { ConvertTo-PodeRoute -Module Example -Commands 'Get-ChildItem' } | Should -Throw -ExpectedMessage '*does not contain function*' + { ConvertTo-PodeRoute -Module Example -Commands 'Get-ChildItem' } | Should -Throw -ExpectedMessage ($PodeLocale.moduleDoesNotContainFunctionExceptionMessage -f 'Example', 'Get-ChildItem') #'*does not contain function*' } It 'Throws error for no commands' { - { ConvertTo-PodeRoute } | Should -Throw -ExpectedMessage 'No commands supplied to convert to Routes' + { ConvertTo-PodeRoute } | Should -Throw -ExpectedMessage $PodeLocale.noCommandsSuppliedToConvertToRoutesExceptionMessage # No commands supplied to convert to Routes. } It 'Calls Add-PodeRoute twice for commands' { @@ -592,6 +635,11 @@ Describe 'ConvertTo-PodeRoute' { Assert-MockCalled Add-PodeRoute -Times 2 -Scope It } + It 'Calls Add-PodeRoute twice for commands by pipe' { + @('Get-ChildItem', 'Invoke-Expression') | ConvertTo-PodeRoute -NoOpenApi + Assert-MockCalled Add-PodeRoute -Times 2 -Scope It + } + It 'Calls Add-PodeRoute twice for module commands' { ConvertTo-PodeRoute -Module Example -NoOpenApi Assert-MockCalled Add-PodeRoute -Times 2 -Scope It @@ -609,16 +657,16 @@ Describe 'Add-PodePage' { } It 'Throws error for invalid Name' { - { Add-PodePage -Name 'Rick+Morty' -ScriptBlock {} } | Should -Throw -ExpectedMessage '*should be a valid alphanumeric*' + { Add-PodePage -Name 'Rick+Morty' -ScriptBlock {} } | Should -Throw -ExpectedMessage ($PodeLocale.pageNameShouldBeAlphaNumericExceptionMessage -f 'Rick+Morty' ) #'*should be a valid alphanumeric*' } It 'Throws error for invalid ScriptBlock' { - { Add-PodePage -Name 'RickMorty' -ScriptBlock {} } | Should -Throw -ExpectedMessage '*non-empty scriptblock is required*' + { Add-PodePage -Name 'RickMorty' -ScriptBlock {} } | Should -Throw -ExpectedMessage $PodeLocale.nonEmptyScriptBlockRequiredForPageRouteExceptionMessage #'*non-empty scriptblock is required*' } It 'Throws error for invalid FilePath' { $PodeContext.Server = @{ 'Root' = $pwd } - { Add-PodePage -Name 'RickMorty' -FilePath './fake/path' } | Should -Throw -ExpectedMessage '*the path does not exist*' + { Add-PodePage -Name 'RickMorty' -FilePath './fake/path' } | Should -Throw -ExpectedMessage ($PodeLocale.pathNotExistExceptionMessage -f '*/fake/path') #'*the path does not exist*' } It 'Call Add-PodeRoute once for ScriptBlock page' { @@ -638,121 +686,121 @@ Describe 'Add-PodePage' { } } -Describe 'Update-PodeRouteSlashes' { +Describe 'Update-PodeRouteSlash' { Context 'Static' { It 'Update route slashes' { $in = '/route' - Update-PodeRouteSlashes -Path $in -Static | Should -Be '/route[/]{0,1}(?.*)' + Update-PodeRouteSlash -Path $in -Static | Should -Be '/route[/]{0,1}(?.*)' } It 'Update route slashes, no slash' { $in = 'route' - Update-PodeRouteSlashes -Path $in -Static | Should -Be '/route[/]{0,1}(?.*)' + Update-PodeRouteSlash -Path $in -Static | Should -Be '/route[/]{0,1}(?.*)' } It 'Update route slashes, ending with wildcard' { $in = '/route/*' - Update-PodeRouteSlashes -Path $in -Static | Should -Be '/route[/]{0,1}(?.*)' + Update-PodeRouteSlash -Path $in -Static | Should -Be '/route[/]{0,1}(?.*)' } It 'Update route slashes, ending with wildcard, no slash' { $in = 'route/*' - Update-PodeRouteSlashes -Path $in -Static | Should -Be '/route[/]{0,1}(?.*)' + Update-PodeRouteSlash -Path $in -Static | Should -Be '/route[/]{0,1}(?.*)' } It 'Update route slashes, with midpoint wildcard' { $in = '/route/*/ending' - Update-PodeRouteSlashes -Path $in -Static | Should -Be '/route/.*/ending[/]{0,1}(?.*)' + Update-PodeRouteSlash -Path $in -Static | Should -Be '/route/.*/ending[/]{0,1}(?.*)' } It 'Update route slashes, with midpoint wildcard, no slash' { $in = 'route/*/ending' - Update-PodeRouteSlashes -Path $in -Static | Should -Be '/route/.*/ending[/]{0,1}(?.*)' + Update-PodeRouteSlash -Path $in -Static | Should -Be '/route/.*/ending[/]{0,1}(?.*)' } It 'Update route slashes, with midpoint wildcard, ending with wildcard' { $in = '/route/*/ending/*' - Update-PodeRouteSlashes -Path $in -Static | Should -Be '/route/.*/ending[/]{0,1}(?.*)' + Update-PodeRouteSlash -Path $in -Static | Should -Be '/route/.*/ending[/]{0,1}(?.*)' } It 'Update route slashes, with midpoint wildcard, ending with wildcard, no slash' { $in = 'route/*/ending/*' - Update-PodeRouteSlashes -Path $in -Static | Should -Be '/route/.*/ending[/]{0,1}(?.*)' + Update-PodeRouteSlash -Path $in -Static | Should -Be '/route/.*/ending[/]{0,1}(?.*)' } } Context 'Non Static' { It 'Update route slashes' { $in = '/route' - Update-PodeRouteSlashes -Path $in | Should -Be '/route' + Update-PodeRouteSlash -Path $in | Should -Be '/route' } It 'Update route slashes, no slash' { $in = 'route' - Update-PodeRouteSlashes -Path $in | Should -Be '/route' + Update-PodeRouteSlash -Path $in | Should -Be '/route' } It 'Update route slashes, ending with wildcard' { $in = '/route/*' - Update-PodeRouteSlashes -Path $in | Should -Be '/route/.*' + Update-PodeRouteSlash -Path $in | Should -Be '/route/.*' } It 'Update route slashes, ending with wildcard, no slash' { $in = 'route/*' - Update-PodeRouteSlashes -Path $in | Should -Be '/route/.*' + Update-PodeRouteSlash -Path $in | Should -Be '/route/.*' } It 'Update route slashes, with midpoint wildcard' { $in = '/route/*/ending' - Update-PodeRouteSlashes -Path $in | Should -Be '/route/.*/ending' + Update-PodeRouteSlash -Path $in | Should -Be '/route/.*/ending' } It 'Update route slashes, with midpoint wildcard, no slash' { $in = 'route/*/ending' - Update-PodeRouteSlashes -Path $in | Should -Be '/route/.*/ending' + Update-PodeRouteSlash -Path $in | Should -Be '/route/.*/ending' } It 'Update route slashes, with midpoint wildcard, ending with wildcard' { $in = '/route/*/ending/*' - Update-PodeRouteSlashes -Path $in | Should -Be '/route/.*/ending/.*' + Update-PodeRouteSlash -Path $in | Should -Be '/route/.*/ending/.*' } It 'Update route slashes, with midpoint wildcard, ending with wildcard, no slash' { $in = 'route/*/ending/*' - Update-PodeRouteSlashes -Path $in | Should -Be '/route/.*/ending/.*' + Update-PodeRouteSlash -Path $in | Should -Be '/route/.*/ending/.*' } } } -Describe 'Resolve-PodePlaceholders' { +Describe 'Resolve-PodePlaceholder' { It 'Update route placeholders, basic' { $in = 'route' - Resolve-PodePlaceholders -Path $in | Should -Be 'route' + Resolve-PodePlaceholder -Path $in | Should -Be 'route' } It 'Update route placeholders' { $in = ':route' - Resolve-PodePlaceholders -Path $in | Should -Be '(?[^\/]+?)' + Resolve-PodePlaceholder -Path $in | Should -Be '(?[^\/]+?)' } It 'Update route placeholders, double with no spacing' { $in = ':route:placeholder' - Resolve-PodePlaceholders -Path $in | Should -Be '(?[^\/]+?)(?[^\/]+?)' + Resolve-PodePlaceholder -Path $in | Should -Be '(?[^\/]+?)(?[^\/]+?)' } It 'Update route placeholders, double with double ::' { $in = '::route:placeholder' - Resolve-PodePlaceholders -Path $in | Should -Be ':(?[^\/]+?)(?[^\/]+?)' + Resolve-PodePlaceholder -Path $in | Should -Be ':(?[^\/]+?)(?[^\/]+?)' } It 'Update route placeholders, double with slash' { $in = ':route/:placeholder' - Resolve-PodePlaceholders -Path $in | Should -Be '(?[^\/]+?)/(?[^\/]+?)' + Resolve-PodePlaceholder -Path $in | Should -Be '(?[^\/]+?)/(?[^\/]+?)' } It 'Update route placeholders, no update' { $in = ': route' - Resolve-PodePlaceholders -Path $in | Should -Be ': route' + Resolve-PodePlaceholder -Path $in | Should -Be ': route' } } @@ -1050,15 +1098,15 @@ Describe 'ConvertTo-PodeMiddleware' { } It 'Errors for invalid middleware type' { - { ConvertTo-PodeMiddleware -Middleware 'string' -PSSession $_PSSession } | Should -Throw -ExpectedMessage '*invalid type*' + { ConvertTo-PodeMiddleware -Middleware 'string' -PSSession $_PSSession } | Should -Throw -ExpectedMessage ($PodeLocale.invalidMiddlewareTypeExceptionMessage -f 'string') # '*invalid type*' } It 'Errors for invalid middleware hashtable - no logic' { - { ConvertTo-PodeMiddleware -Middleware @{} -PSSession $_PSSession } | Should -Throw -ExpectedMessage '*no logic defined*' + { ConvertTo-PodeMiddleware -Middleware @{} -PSSession $_PSSession } | Should -Throw -ExpectedMessage $PodeLocale.hashtableMiddlewareNoLogicExceptionMessage # '*no logic defined*' } It 'Errors for invalid middleware hashtable - logic not scriptblock' { - { ConvertTo-PodeMiddleware -Middleware @{ Logic = 'string' } -PSSession $_PSSession } | Should -Throw -ExpectedMessage '*invalid logic type*' + { ConvertTo-PodeMiddleware -Middleware @{ Logic = 'string' } -PSSession $_PSSession } | Should -Throw -ExpectedMessage ($PodeLocale.invalidLogicTypeInHashtableMiddlewareExceptionMessage -f 'string') #'*invalid logic type*' } It 'Returns hashtable for single hashtable middleware' { diff --git a/tests/unit/Schedules.Tests.ps1 b/tests/unit/Schedules.Tests.ps1 index 05e688db1..4f854672a 100644 --- a/tests/unit/Schedules.Tests.ps1 +++ b/tests/unit/Schedules.Tests.ps1 @@ -1,19 +1,21 @@ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] param() BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' } Describe 'Find-PodeSchedule' { Context 'Invalid parameters supplied' { It 'Throw null name parameter error' { - { Find-PodeSchedule -Name $null } | Should -Throw -ExpectedMessage '*The argument is null or empty*' + { Find-PodeSchedule -Name $null } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Find-PodeSchedule' } It 'Throw empty name parameter error' { - { Find-PodeSchedule -Name ([string]::Empty) } | Should -Throw -ExpectedMessage '*The argument is null or empty*' + { Find-PodeSchedule -Name ([string]::Empty) } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Find-PodeSchedule' } } @@ -35,25 +37,27 @@ Describe 'Find-PodeSchedule' { Describe 'Add-PodeSchedule' { BeforeAll { - Mock 'ConvertFrom-PodeCronExpression' { @{} } Mock 'Get-PodeCronNextEarliestTrigger' { [datetime]::new(2020, 1, 1) } } It 'Throws error because schedule already exists' { $PodeContext = @{ 'Schedules' = @{ Items = @{ 'test' = $null }; } } - { Add-PodeSchedule -Name 'test' -Cron '@hourly' -ScriptBlock {} } | Should -Throw -ExpectedMessage '*already defined*' + $expectedMessage = ($PodeLocale.scheduleAlreadyDefinedExceptionMessage -f 'test' ).Replace('[', '`[').Replace(']', '`]') # -replace '\[', '`[' -replace '\]', '`]' + { Add-PodeSchedule -Name 'test' -Cron '@hourly' -ScriptBlock {} } | Should -Throw -ExpectedMessage $expectedMessage #'*already defined*' } It 'Throws error because end time in the past' { $PodeContext = @{ 'Schedules' = @{ Items = @{} }; } $end = ([DateTime]::Now.AddHours(-1)) - { Add-PodeSchedule -Name 'test' -Cron '@hourly' -ScriptBlock {} -EndTime $end } | Should -Throw -ExpectedMessage '*the EndTime value must be in the future*' + $expectedMessage = ($PodeLocale.timerParameterMustBeGreaterscheduleEndTimeMustBeInFutureExceptionMessageThanZeroExceptionMessage -f 'test' ).Replace('[', '`[').Replace(']', '`]') # -replace '\[', '`[' -replace '\]', '`]' + { Add-PodeSchedule -Name 'test' -Cron '@hourly' -ScriptBlock {} -EndTime $end } | Should -Throw -ExpectedMessage $expectedMessage #'*the EndTime value must be in the future*' } It 'Throws error because start time is after end time' { $PodeContext = @{ 'Schedules' = @{ Items = @{} }; } $start = ([DateTime]::Now.AddHours(3)) $end = ([DateTime]::Now.AddHours(1)) - { Add-PodeSchedule -Name 'test' -Cron '@hourly' -ScriptBlock {} -StartTime $start -EndTime $end } | Should -Throw -ExpectedMessage '*starttime after the endtime*' + $expectedMessage = ($PodeLocale.scheduleStartTimeAfterEndTimeExceptionMessage -f 'test').Replace('[', '`[').Replace(']', '`]') # -replace '\[', '`[' -replace '\]', '`]' + { Add-PodeSchedule -Name 'test' -Cron '@hourly' -ScriptBlock {} -StartTime $start -EndTime $end } | Should -Throw -ExpectedMessage $expectedMessage # [Schedule] {0}: Cannot have a 'StartTime' after the 'EndTime' } It 'Adds new schedule supplying everything' { @@ -125,7 +129,7 @@ Describe 'Add-PodeSchedule' { $start = ([DateTime]::Now.AddHours(3)) $end = ([DateTime]::Now.AddHours(5)) - Add-PodeSchedule -Name 'test' -Cron @('@minutely', '@hourly') -ScriptBlock { Write-Host 'hello' } -StartTime $start -EndTime $end + Add-PodeSchedule -Name 'test' -Cron @('@minutely', '@hourly') -ScriptBlock { Write-Host 'hello' } -StartTime $start -EndTime $end $schedule = $PodeContext.Schedules.Items['test'] $schedule | Should -Not -Be $null diff --git a/tests/unit/Security.Tests.ps1 b/tests/unit/Security.Tests.ps1 index 9862b9510..c1311c401 100644 --- a/tests/unit/Security.Tests.ps1 +++ b/tests/unit/Security.Tests.ps1 @@ -1,17 +1,19 @@ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] param() BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' $PodeContext = @{ 'Server' = $null; } } Describe 'Test-PodeIPAccess' { Context 'Invalid parameters' { It 'Throws error for invalid IP' { - { Test-PodeIPAccess -IP $null -Limit 1 -Seconds 1 } | Should -Throw -ExpectedMessage '*argument is null*' + { Test-PodeIPAccess -IP $null -Limit 1 -Seconds 1 } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Test-PodeIPAccess' } } } @@ -19,7 +21,7 @@ Describe 'Test-PodeIPAccess' { Describe 'Test-PodeIPLimit' { Context 'Invalid parameters' { It 'Throws error for invalid IP' { - { Test-PodeIPLimit -IP $null -Limit 1 -Seconds 1 } | Should -Throw -ExpectedMessage '*argument is null*' + { Test-PodeIPLimit -IP $null -Limit 1 -Seconds 1 } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Test-PodeIPLimit' } } } @@ -81,23 +83,23 @@ Describe 'Add-PodeAccessRule' { Describe 'Add-PodeIPLimit' { Context 'Invalid parameters' { It 'Throws error for invalid IP' { - { Add-PodeIPLimit -IP $null -Limit 1 -Seconds 1 } | Should -Throw -ExpectedMessage '*because it is an empty string*' + { Add-PodeIPLimit -IP $null -Limit 1 -Seconds 1 } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorEmptyStringNotAllowed,Add-PodeIPLimit' } It 'Throws error for negative limit' { - { Add-PodeIPLimit -IP '127.0.0.1' -Limit -1 -Seconds 1 } | Should -Throw -ExpectedMessage '*0 or less*' + { Add-PodeIPLimit -IP '127.0.0.1' -Limit -1 -Seconds 1 } | Should -Throw -ExpectedMessage ($PodeLocale.limitValueCannotBeZeroOrLessExceptionMessage -f '127.0.0.1') #'*0 or less*' } It 'Throws error for negative seconds' { - { Add-PodeIPLimit -IP '127.0.0.1' -Limit 1 -Seconds -1 } | Should -Throw -ExpectedMessage '*0 or less*' + { Add-PodeIPLimit -IP '127.0.0.1' -Limit 1 -Seconds -1 } | Should -Throw -ExpectedMessage ($PodeLocale.secondsValueCannotBeZeroOrLessExceptionMessage -f '127.0.0.1') #'*0 or less*' } It 'Throws error for zero limit' { - { Add-PodeIPLimit -IP '127.0.0.1' -Limit 0 -Seconds 1 } | Should -Throw -ExpectedMessage '*0 or less*' + { Add-PodeIPLimit -IP '127.0.0.1' -Limit 0 -Seconds 1 } | Should -Throw -ExpectedMessage ($PodeLocale.limitValueCannotBeZeroOrLessExceptionMessage -f '127.0.0.1') #'*0 or less*' } It 'Throws error for zero seconds' { - { Add-PodeIPLimit -IP '127.0.0.1' -Limit 1 -Seconds 0 } | Should -Throw -ExpectedMessage '*0 or less*' + { Add-PodeIPLimit -IP '127.0.0.1' -Limit 1 -Seconds 0 } | Should -Throw -ExpectedMessage ($PodeLocale.secondsValueCannotBeZeroOrLessExceptionMessage -f '127.0.0.1') #'*0 or less*' } } @@ -194,7 +196,7 @@ Describe 'Add-PodeIPLimit' { It 'Throws error for invalid IP' { $PodeContext.Server = @{ 'Limits' = @{ 'Rules' = @{}; 'Active' = @{}; } } - { Add-PodeIPLimit -IP '256.0.0.0' -Limit 1 -Seconds 1 } | Should -Throw -ExpectedMessage '*invalid ip address*' + { Add-PodeIPLimit -IP '256.0.0.0' -Limit 1 -Seconds 1 } | Should -Throw -ErrorId 'FormatException,Get-PodeIPAddress' } } } @@ -389,7 +391,7 @@ Describe 'Add-PodeIPAccess' { It 'Throws error for invalid IP' { $PodeContext.Server = @{ 'Access' = @{ 'Allow' = @{}; 'Deny' = @{}; } } - { Add-PodeIPAccess -Access 'Allow' -IP '256.0.0.0' } | Should -Throw -ExpectedMessage '*invalid ip address*' + { Add-PodeIPAccess -Access 'Allow' -IP '256.0.0.0' } | Should -Throw -ErrorId 'FormatException,Get-PodeIPAddress' } } } @@ -580,7 +582,7 @@ Describe 'New-PodeCsrfToken' { } } - { New-PodeCsrfToken } | Should -Throw -ExpectedMessage '*not been initialised*' + { New-PodeCsrfToken } | Should -Throw -ExpectedMessage $PodeLocale.csrfMiddlewareNotInitializedExceptionMessage #CSRF Middleware has not been initialized. } diff --git a/tests/unit/Server.Tests.ps1 b/tests/unit/Server.Tests.ps1 index ff9bd1327..6056b581c 100644 --- a/tests/unit/Server.Tests.ps1 +++ b/tests/unit/Server.Tests.ps1 @@ -5,6 +5,7 @@ BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' $PodeContext = @{ @@ -18,7 +19,7 @@ Describe 'Start-PodeInternalServer' { Mock Add-PodePSInbuiltDrive { } Mock Invoke-PodeScriptBlock { } Mock New-PodeRunspaceState { } - Mock New-PodeRunspacePools { } + Mock New-PodeRunspacePool { } Mock Start-PodeLoggingRunspace { } Mock Start-PodeTimerRunspace { } Mock Start-PodeScheduleRunspace { } @@ -36,6 +37,7 @@ Describe 'Start-PodeInternalServer' { Mock Invoke-PodeEvent { } Mock Write-Verbose { } Mock Add-PodeScopedVariablesInbuilt { } + Mock Write-PodeHost { } } It 'Calls one-off script logic' { @@ -43,7 +45,7 @@ Describe 'Start-PodeInternalServer' { Start-PodeInternalServer | Out-Null Assert-MockCalled Invoke-PodeScriptBlock -Times 1 -Scope It - Assert-MockCalled New-PodeRunspacePools -Times 1 -Scope It + Assert-MockCalled New-PodeRunspacePool -Times 1 -Scope It Assert-MockCalled New-PodeRunspaceState -Times 1 -Scope It Assert-MockCalled Start-PodeTimerRunspace -Times 1 -Scope It Assert-MockCalled Start-PodeScheduleRunspace -Times 1 -Scope It @@ -57,7 +59,7 @@ Describe 'Start-PodeInternalServer' { Start-PodeInternalServer | Out-Null Assert-MockCalled Invoke-PodeScriptBlock -Times 1 -Scope It - Assert-MockCalled New-PodeRunspacePools -Times 1 -Scope It + Assert-MockCalled New-PodeRunspacePool -Times 1 -Scope It Assert-MockCalled New-PodeRunspaceState -Times 1 -Scope It Assert-MockCalled Start-PodeTimerRunspace -Times 1 -Scope It Assert-MockCalled Start-PodeScheduleRunspace -Times 1 -Scope It @@ -71,7 +73,7 @@ Describe 'Start-PodeInternalServer' { Start-PodeInternalServer | Out-Null Assert-MockCalled Invoke-PodeScriptBlock -Times 1 -Scope It - Assert-MockCalled New-PodeRunspacePools -Times 1 -Scope It + Assert-MockCalled New-PodeRunspacePool -Times 1 -Scope It Assert-MockCalled New-PodeRunspaceState -Times 1 -Scope It Assert-MockCalled Start-PodeTimerRunspace -Times 1 -Scope It Assert-MockCalled Start-PodeScheduleRunspace -Times 1 -Scope It @@ -85,7 +87,7 @@ Describe 'Start-PodeInternalServer' { Start-PodeInternalServer | Out-Null Assert-MockCalled Invoke-PodeScriptBlock -Times 1 -Scope It - Assert-MockCalled New-PodeRunspacePools -Times 1 -Scope It + Assert-MockCalled New-PodeRunspacePool -Times 1 -Scope It Assert-MockCalled New-PodeRunspaceState -Times 1 -Scope It Assert-MockCalled Start-PodeTimerRunspace -Times 1 -Scope It Assert-MockCalled Start-PodeScheduleRunspace -Times 1 -Scope It @@ -98,19 +100,20 @@ Describe 'Start-PodeInternalServer' { Describe 'Restart-PodeInternalServer' { BeforeAll { Mock Write-Host { } - Mock Close-PodeRunspaces { } - Mock Remove-PodePSDrives { } + Mock Close-PodeRunspace { } + Mock Remove-PodePSDrive { } Mock Open-PodeConfiguration { return $null } Mock Start-PodeInternalServer { } Mock Write-PodeErrorLog { } Mock Close-PodeDisposable { } Mock Invoke-PodeEvent { } } + It 'Resetting the server values' { $PodeContext = @{ Tokens = @{ - Cancellation = New-Object System.Threading.CancellationTokenSource - Restart = New-Object System.Threading.CancellationTokenSource + Cancellation = [System.Threading.CancellationTokenSource]::new() + Restart = [System.Threading.CancellationTokenSource]::new() } Server = @{ Routes = @{ @@ -220,11 +223,11 @@ Describe 'Restart-PodeInternalServer' { Processes = @{} } Tasks = @{ - Enabled = $true - Items = @{ + Enabled = $true + Items = @{ key = 'value' } - Results = @{} + Processes = @{} } Fim = @{ Enabled = $true diff --git a/tests/unit/Serverless.Tests.ps1 b/tests/unit/Serverless.Tests.ps1 index 6366d7895..66f29c59c 100644 --- a/tests/unit/Serverless.Tests.ps1 +++ b/tests/unit/Serverless.Tests.ps1 @@ -5,6 +5,7 @@ BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' } Describe 'Start-PodeAzFuncServer' { BeforeAll { @@ -16,17 +17,17 @@ Describe 'Start-PodeAzFuncServer' { Mock Get-PodeRouteValidateMiddleware { } Mock Get-PodeBodyMiddleware { } Mock Get-PodeCookieMiddleware { } - Mock New-Object { return @{} } + Mock New-PodeAzFuncResponse { return @{} } Mock Get-PodeHeader { return 'some-value' } Mock Invoke-PodeScriptBlock { } Mock Write-Host { } Mock Invoke-PodeEndware { } Mock Set-PodeServerHeader { } Mock Set-PodeResponseStatus { } - Mock Update-PodeServerRequestMetrics { } + Mock Update-PodeServerRequestMetric { } } It 'Throws error for null data' { - { Start-PodeAzFuncServer -Data $null } | Should -Throw -ExpectedMessage '*because it is null*' + { Start-PodeAzFuncServer -Data $null } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorNullNotAllowed,Start-PodeAzFuncServer' } It 'Runs the server, fails middleware with no route' { @@ -154,10 +155,10 @@ Describe 'Start-PodeAwsLambdaServer' { Mock Invoke-PodeEndware { } Mock Set-PodeServerHeader { } Mock Set-PodeResponseStatus { } - Mock Update-PodeServerRequestMetrics { } } + Mock Update-PodeServerRequestMetric { } } It 'Throws error for null data' { - { Start-PodeAwsLambdaServer -Data $null } | Should -Throw -ExpectedMessage '*because it is null*' + { Start-PodeAwsLambdaServer -Data $null } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorNullNotAllowed,Start-PodeAwsLambdaServer' } It 'Runs the server, fails middleware with no route' { diff --git a/tests/unit/Sessions.Tests.ps1 b/tests/unit/Sessions.Tests.ps1 index 5b5deb390..dcf9a77b6 100644 --- a/tests/unit/Sessions.Tests.ps1 +++ b/tests/unit/Sessions.Tests.ps1 @@ -5,6 +5,7 @@ BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' $now = [datetime]::UtcNow } @@ -20,7 +21,7 @@ Describe 'Get-PodeSession' { } } - { Get-PodeSession } | Should -Throw -ExpectedMessage '*because it is an empty string*' + { Get-PodeSession } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorEmptyStringNotAllowed,Test-PodeCookieSigned' } It 'Throws an empry string value error' { @@ -32,7 +33,7 @@ Describe 'Get-PodeSession' { } } - { Get-PodeSession } | Should -Throw -ExpectedMessage '*because it is an empty string*' + { Get-PodeSession } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorEmptyStringNotAllowed,Test-PodeCookieSigned' } } @@ -108,7 +109,7 @@ Describe 'Get-PodeSession' { Describe 'Set-PodeSessionDataHash' { Context 'Invalid parameters supplied' { It 'Throws null value error' { - { Set-PodeSessionDataHash } | Should -Throw -ExpectedMessage '*No session available*' + { Set-PodeSessionDataHash } | Should -Throw -ExpectedMessage $PodeLocale.noSessionToCalculateDataHashExceptionMessage #'*No session available*' } } @@ -258,7 +259,7 @@ Describe 'Set-PodeSession' { Describe 'Remove-PodeSession' { It 'Throws an error if sessions are not configured' { Mock Test-PodeSessionsEnabled { return $false } - { Remove-PodeSession } | Should -Throw 'Sessions have not been configured' + { Remove-PodeSession } | Should -Throw $PodeLocale.sessionsNotConfiguredExceptionMessage # Sessions have not been configured. } It 'Does nothing if there is no session' { @@ -285,13 +286,13 @@ Describe 'Remove-PodeSession' { Describe 'Save-PodeSession' { It 'Throws an error if sessions are not configured' { Mock Test-PodeSessionsEnabled { return $false } - { Save-PodeSession } | Should -Throw 'Sessions have not been configured' + { Save-PodeSession } | Should -Throw $PodeLocale.sessionsNotConfiguredExceptionMessage # Sessions have not been configured. } It 'Throws error if there is no session' { Mock Test-PodeSessionsEnabled { return $true } $WebEvent = @{} - { Save-PodeSession } | Should -Throw -ExpectedMessage 'There is no session available to save' + { Save-PodeSession } | Should -Throw -ExpectedMessage $PodeLocale.noSessionAvailableToSaveExceptionMessage # There is no session available to save. } It 'Call saves the session' { diff --git a/tests/unit/State.Tests.ps1 b/tests/unit/State.Tests.ps1 index b7feec3b5..f8c2ff61b 100644 --- a/tests/unit/State.Tests.ps1 +++ b/tests/unit/State.Tests.ps1 @@ -5,6 +5,7 @@ BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' $PodeContext = @{ 'Server' = $null; } } @@ -12,7 +13,7 @@ BeforeAll { Describe 'Set-PodeState' { It 'Throws error when not initialised' { $PodeContext.Server = @{ 'State' = $null } - { Set-PodeState -Name 'test' } | Should -Throw -ExpectedMessage 'Pode has not been initialised' + { Set-PodeState -Name 'test' } | Should -Throw -ExpectedMessage $PodeLocale.podeNotInitializedExceptionMessage # Pode has not been initialized. } It 'Sets and returns an object' { @@ -23,12 +24,21 @@ Describe 'Set-PodeState' { $PodeContext.Server.State['test'].Value | Should -Be 7 $PodeContext.Server.State['test'].Scope | Should -Be @() } + + It 'Sets by pipe and returns an object array' { + $PodeContext.Server = @{ 'State' = @{} } + $result = @(7,3,4)|Set-PodeState -Name 'test' + + $result | Should -Be @(7,3,4) + $PodeContext.Server.State['test'].Value | Should -Be @(7,3,4) + $PodeContext.Server.State['test'].Scope | Should -Be @() + } } Describe 'Get-PodeState' { It 'Throws error when not initialised' { $PodeContext.Server = @{ 'State' = $null } - { Get-PodeState -Name 'test' } | Should -Throw -ExpectedMessage 'Pode has not been initialised' + { Get-PodeState -Name 'test' } | Should -Throw -ExpectedMessage $PodeLocale.podeNotInitializedExceptionMessage # Pode has not been initialized. } It 'Gets an object from the state' { @@ -41,7 +51,7 @@ Describe 'Get-PodeState' { Describe 'Remove-PodeState' { It 'Throws error when not initialised' { $PodeContext.Server = @{ 'State' = $null } - { Remove-PodeState -Name 'test' } | Should -Throw -ExpectedMessage 'Pode has not been initialised' + { Remove-PodeState -Name 'test' } | Should -Throw -ExpectedMessage $PodeLocale.podeNotInitializedExceptionMessage # Pode has not been initialized. } It 'Removes an object from the state' { @@ -55,7 +65,7 @@ Describe 'Remove-PodeState' { Describe 'Save-PodeState' { It 'Throws error when not initialised' { $PodeContext.Server = @{ 'State' = $null } - { Save-PodeState -Path 'some/path' } | Should -Throw -ExpectedMessage 'Pode has not been initialised' + { Save-PodeState -Path 'some/path' } | Should -Throw -ExpectedMessage $PodeLocale.podeNotInitializedExceptionMessage # Pode has not been initialized. } It 'Saves the state to file' { @@ -95,7 +105,7 @@ Describe 'Save-PodeState' { Describe 'Restore-PodeState' { It 'Throws error when not initialised' { $PodeContext.Server = @{ 'State' = $null } - { Restore-PodeState -Path 'some/path' } | Should -Throw -ExpectedMessage 'Pode has not been initialised' + { Restore-PodeState -Path 'some/path' } | Should -Throw -ExpectedMessage $PodeLocale.podeNotInitializedExceptionMessage # Pode has not been initialized. } It 'Restores the state from file' { @@ -112,7 +122,7 @@ Describe 'Restore-PodeState' { Describe 'Test-PodeState' { It 'Throws error when not initialised' { $PodeContext.Server = @{ 'State' = $null } - { Test-PodeState -Name 'test' } | Should -Throw -ExpectedMessage 'Pode has not been initialised' + { Test-PodeState -Name 'test' } | Should -Throw -ExpectedMessage $PodeLocale.podeNotInitializedExceptionMessage # Pode has not been initialized. } It 'Returns true for an object being in the state' { diff --git a/tests/unit/Timers.Tests.ps1 b/tests/unit/Timers.Tests.ps1 index c8b6e6076..f8607c73f 100644 --- a/tests/unit/Timers.Tests.ps1 +++ b/tests/unit/Timers.Tests.ps1 @@ -1,10 +1,12 @@ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] param() BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' $PodeContext = @{ 'Server' = $null; } } @@ -12,11 +14,11 @@ BeforeAll { Describe 'Find-PodeTimer' { Context 'Invalid parameters supplied' { It 'Throw null name parameter error' { - { Find-PodeTimer -Name $null } | Should -Throw -ExpectedMessage '*The argument is null or empty*' + { Find-PodeTimer -Name $null } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Find-PodeTimer' } It 'Throw empty name parameter error' { - { Find-PodeTimer -Name ([string]::Empty) } | Should -Throw -ExpectedMessage '*The argument is null or empty*' + { Find-PodeTimer -Name ([string]::Empty) } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Find-PodeTimer' } } @@ -39,32 +41,37 @@ Describe 'Find-PodeTimer' { Describe 'Add-PodeTimer' { It 'Throws error because timer already exists' { $PodeContext = @{ 'Timers' = @{ Items = @{ 'test' = $null }; } } - { Add-PodeTimer -Name 'test' -Interval 1 -ScriptBlock {} } | Should -Throw -ExpectedMessage '*already defined*' + $expectedMessage = ($PodeLocale.timerAlreadyDefinedExceptionMessage -f 'test' ).Replace('[', '`[').Replace(']', '`]') # -replace '\[', '`[' -replace '\]', '`]' + { Add-PodeTimer -Name 'test' -Interval 1 -ScriptBlock {} } | Should -Throw -ExpectedMessage $expectedMessage #'*already defined*' } It 'Throws error because interval is 0' { $PodeContext = @{ 'Timers' = @{ Items = @{} }; } - { Add-PodeTimer -Name 'test' -Interval 0 -ScriptBlock {} } | Should -Throw -ExpectedMessage '*interval must be greater than 0*' + $expectedMessage = ($PodeLocale.timerParameterMustBeGreaterThanZeroExceptionMessage -f 'test', 'interval').Replace('[', '`[').Replace(']', '`]') # -replace '\[', '`[' -replace '\]', '`]' + { Add-PodeTimer -Name 'test' -Interval 0 -ScriptBlock {} } | Should -Throw -ExpectedMessage $expectedMessage #'*interval must be greater than 0*' } It 'Throws error because interval is less than 0' { $PodeContext = @{ 'Timers' = @{ Items = @{} }; } - { Add-PodeTimer -Name 'test' -Interval -1 -ScriptBlock {} } | Should -Throw -ExpectedMessage '*interval must be greater than 0*' + $expectedMessage = ($PodeLocale.timerParameterMustBeGreaterThanZeroExceptionMessage -f 'test', 'interval').Replace('[', '`[').Replace(']', '`]') # -replace '\[', '`[' -replace '\]', '`]' + { Add-PodeTimer -Name 'test' -Interval -1 -ScriptBlock {} } | Should -Throw -ExpectedMessage $expectedMessage #'*interval must be greater than 0*' } It 'Throws error because limit is negative' { $PodeContext = @{ 'Timers' = @{ Items = @{} }; } - { Add-PodeTimer -Name 'test' -Interval 1 -ScriptBlock {} -Limit -1 } | Should -Throw -ExpectedMessage '*negative limit*' + $expectedMessage = ($PodeLocale.timerParameterMustBeGreaterThanZeroExceptionMessage -f 'test', 'Limit').Replace('[', '`[').Replace(']', '`]') # -replace '\[', '`[' -replace '\]', '`]' + { Add-PodeTimer -Name 'test' -Interval 1 -ScriptBlock {} -Limit -1 } | Should -Throw -ExpectedMessage $expectedMessage #[Timer] {0}: {1} must be greater than 0. } It 'Throws error because skip is negative' { $PodeContext = @{ 'Timers' = @{ Items = @{} }; } - { Add-PodeTimer -Name 'test' -Interval 1 -ScriptBlock {} -Skip -1 } | Should -Throw -ExpectedMessage '*negative skip*' + $expectedMessage = ($PodeLocale.timerParameterMustBeGreaterThanZeroExceptionMessage -f 'test', 'skip').Replace('[', '`[').Replace(']', '`]') # -replace '\[', '`[' -replace '\]', '`]' + { Add-PodeTimer -Name 'test' -Interval 1 -ScriptBlock {} -Skip -1 } | Should -Throw -ExpectedMessage $expectedMessage #[Timer] {0}: {1} must be greater than 0. } It 'Adds new timer to session with no limit' { $PodeContext = @{ 'Timers' = @{ Items = @{} }; } - Add-PodeTimer -Name 'test' -Interval 1 -ScriptBlock { Write-Host 'hello' } -Limit 0 -Skip 1 + Add-PodeTimer -Name 'test' -Interval 1 -ScriptBlock { Write-Host 'hello' } -Limit 0 -Skip 1 $timer = $PodeContext.Timers.Items['test'] $timer | Should -Not -Be $null diff --git a/tests/unit/_.Tests.ps1 b/tests/unit/_.Tests.ps1 index 19798c73b..61e8aad23 100644 --- a/tests/unit/_.Tests.ps1 +++ b/tests/unit/_.Tests.ps1 @@ -1,3 +1,25 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() + +BeforeDiscovery { + $path = $PSCommandPath + $examplesPath = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/examples/' + + # List of directories to exclude + $excludeDirs = @('scripts', 'views', 'static', 'public', 'assets', 'timers', 'modules', + 'Authentication', 'certs', 'logs', 'relative', 'routes', 'issues') + + # Convert exlusion list into single regex pattern for directory matching + $dirSeparator = [IO.Path]::DirectorySeparatorChar + $excludeDirs = "\$($dirSeparator)($($excludeDirs -join '|'))\$($dirSeparator)" + + # get the example scripts + $ps1Files = @(Get-ChildItem -Path $examplesPath -Filter *.ps1 -Recurse -File -Force | + Where-Object { + $_.FullName -inotmatch $excludeDirs + }).FullName +} + BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' @@ -120,4 +142,38 @@ Describe 'All Aliases' { $found | Should -Be @() } +} + + +Describe 'Examples Script Headers' { + Context 'Checking file: [<_>]' -ForEach ($ps1Files) { + BeforeAll { + $content = Get-Content -Path $_ -Raw + } + It 'should have a .SYNOPSIS section' { + $hasSynopsis = $content -match '\.SYNOPSIS\s+([^\#]*)' + $hasSynopsis | Should -Be $true + } + + It 'should have a .DESCRIPTION section' { + $hasDescription = $content -match '\.DESCRIPTION\s+([^\#]*)' + $hasDescription | Should -Be $true + } + + It 'should have a .NOTES section with Author and License' { + $hasNotes = $content -match '\.NOTES\s+([^\#]*?)Author:\s*Pode Team\s*License:\s*MIT License' + $hasNotes | Should -Be $true + } + + It 'should have a .LINK section' { + $hasDescription = $content -match '\.LINK\s+([^\#]*)' + $hasDescription | Should -Be $true + } + + It 'should have a .EXAMPLE section' { + $hasDescription = $content -match '\.EXAMPLE\s+([^\#]*)' + $hasDescription | Should -Be $true + } + } + } \ No newline at end of file