mirror of
https://github.com/wiremock/WireMock.Net.git
synced 2026-01-11 22:30:41 +01:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7310fbc7b | ||
|
|
8a07286b89 | ||
|
|
9392069f8a | ||
|
|
0fd190b5a3 | ||
|
|
4368e3cde6 | ||
|
|
fc0f82db33 | ||
|
|
beabba4064 | ||
|
|
d9a7e80360 | ||
|
|
66a048a487 | ||
|
|
04d53f3a9e | ||
|
|
a8562fda32 | ||
|
|
5abb424d3c | ||
|
|
db158bcc7e | ||
|
|
0effda3cfa | ||
|
|
ff36c1ee6f |
19
CHANGELOG.md
19
CHANGELOG.md
@@ -1,3 +1,17 @@
|
||||
# 1.8.0 (28 April 2025)
|
||||
- [#1268](https://github.com/WireMock-Net/WireMock.Net/pull/1268) - 1.7.x [bug] contributed by [StefH](https://github.com/StefH)
|
||||
- [#1271](https://github.com/WireMock-Net/WireMock.Net/pull/1271) - Add HandlebarsSettings [feature] contributed by [StefH](https://github.com/StefH)
|
||||
- [#1273](https://github.com/WireMock-Net/WireMock.Net/pull/1273) - feat(awesome-assertions): Added new project WireMock.Net.AwesomeAssertions [feature] contributed by [Crowbar90](https://github.com/Crowbar90)
|
||||
- [#1278](https://github.com/WireMock-Net/WireMock.Net/pull/1278) - changed null check in JSONPathMatcher and JmesPathMatcher to ensure t… [bug] contributed by [jollyjoyce1995](https://github.com/jollyjoyce1995)
|
||||
- [#1283](https://github.com/WireMock-Net/WireMock.Net/pull/1283) - Add an launch inspector command to Aspire Dashboard [feature] contributed by [jmezach](https://github.com/jmezach)
|
||||
- [#1285](https://github.com/WireMock-Net/WireMock.Net/pull/1285) - Enable support for WireMock Middleware in Hosted Services [bug] contributed by [etkr](https://github.com/etkr)
|
||||
- [#1243](https://github.com/WireMock-Net/WireMock.Net/issues/1243) - Support for AwesomeAssertions [feature]
|
||||
- [#1264](https://github.com/WireMock-Net/WireMock.Net/issues/1264) - OpenApiParser - Construction of path possibly incorrect [bug]
|
||||
- [#1275](https://github.com/WireMock-Net/WireMock.Net/issues/1275) - WithMappingFromOpenApiFile - Support for OpenAPI 3.1.0 [feature]
|
||||
- [#1276](https://github.com/WireMock-Net/WireMock.Net/issues/1276) - Add WireMock Inspector command to Aspire integration [feature]
|
||||
- [#1277](https://github.com/WireMock-Net/WireMock.Net/issues/1277) - Newtonsoft.Json.JsonReaderException: 'Unable to read the JSON string.' when using IStringMatcher with an empty body [bug]
|
||||
- [#1284](https://github.com/WireMock-Net/WireMock.Net/issues/1284) - WireMock.Net.AspNetCore.Middleware does not work in hosted services [bug]
|
||||
|
||||
# 1.7.4 (27 February 2025)
|
||||
- [#1256](https://github.com/WireMock-Net/WireMock.Net/pull/1256) - Add ToArray() to ConcurrentObservableCollection [bug] contributed by [StefH](https://github.com/StefH)
|
||||
- [#1254](https://github.com/WireMock-Net/WireMock.Net/issues/1254) - FindLogEntries exception 'Destination array was not long enough' [bug]
|
||||
@@ -8,7 +22,7 @@
|
||||
|
||||
# 1.7.2 (12 February 2025)
|
||||
- [#1246](https://github.com/WireMock-Net/WireMock.Net/pull/1246) - Add "AddUrl" to WireMockContainerBuilder to support grpc [feature] contributed by [StefH](https://github.com/StefH)
|
||||
- [#1248](https://github.com/WireMock-Net/WireMock.Net/pull/1248) - Add exception message to logging when mapping fails due to an exception. contributed by [JvE-iO](https://github.com/JvE-iO)
|
||||
- [#1248](https://github.com/WireMock-Net/WireMock.Net/pull/1248) - Add exception message to logging when mapping fails due to an exception. [feature] contributed by [JvE-iO](https://github.com/JvE-iO)
|
||||
- [#1250](https://github.com/WireMock-Net/WireMock.Net/pull/1250) - Add ProtoDefinition to WireMockContainer [feature] contributed by [StefH](https://github.com/StefH)
|
||||
- [#1239](https://github.com/WireMock-Net/WireMock.Net/issues/1239) - How to use WiremockContainerBuilder for grpc using http2 [feature]
|
||||
- [#1249](https://github.com/WireMock-Net/WireMock.Net/issues/1249) - Add protodefinition and refer it from mapping [feature]
|
||||
@@ -991,8 +1005,7 @@
|
||||
- [#263](https://github.com/WireMock-Net/WireMock.Net/issues/263) - Content-Type multipart/form-data is not serialized in proxy and recording mode [bug]
|
||||
|
||||
# 1.0.11.0 (30 March 2019)
|
||||
- [#261](https://github.com/WireMock-Net/WireMock.Net/pull/261) - Fix BodyAsJson transform bug in ResponseMessageTransformer contributed by [ghost](https://github.com/ghost)
|
||||
- [#262](https://github.com/WireMock-Net/WireMock.Net/pull/262) - Add ProvideResponse_WithJsonBodyAndTransform test contributed by [ghost](https://github.com/ghost)
|
||||
- [#261](https://github.com/WireMock-Net/WireMock.Net/pull/261) - Fix BodyAsJson transform bug in ResponseMessageTransformer [bug] contributed by [ghost](https://github.com/ghost)
|
||||
|
||||
# 1.0.10.0 (27 March 2019)
|
||||
- [#260](https://github.com/WireMock-Net/WireMock.Net/pull/260) - Fix Response.Delay property serialization [bug] contributed by [StefH](https://github.com/StefH)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<VersionPrefix>1.7.4</VersionPrefix>
|
||||
<VersionPrefix>1.8.0</VersionPrefix>
|
||||
<PackageIcon>WireMock.Net-Logo.png</PackageIcon>
|
||||
<PackageProjectUrl>https://github.com/WireMock-Net/WireMock.Net</PackageProjectUrl>
|
||||
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
rem https://github.com/StefH/GitHubReleaseNotes
|
||||
|
||||
SET version=1.7.4
|
||||
SET version=1.8.0
|
||||
|
||||
GitHubReleaseNotes --output CHANGELOG.md --skip-empty-releases --exclude-labels test question invalid doc duplicate example environment --version %version% --token %GH_TOKEN%
|
||||
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# 1.7.4 (27 February 2025)
|
||||
- #1256 Add ToArray() to ConcurrentObservableCollection [bug]
|
||||
- #1254 FindLogEntries exception 'Destination array was not long enough' [bug]
|
||||
# 1.8.0 (28 April 2025)
|
||||
- #1268 1.7.x [bug]
|
||||
- #1271 Add HandlebarsSettings [feature]
|
||||
- #1273 feat(awesome-assertions): Added new project WireMock.Net.AwesomeAssertions [feature]
|
||||
- #1278 changed null check in JSONPathMatcher and JmesPathMatcher to ensure t… [bug]
|
||||
- #1283 Add an launch inspector command to Aspire Dashboard [feature]
|
||||
- #1285 Enable support for WireMock Middleware in Hosted Services [bug]
|
||||
- #1243 Support for AwesomeAssertions [feature]
|
||||
- #1264 OpenApiParser - Construction of path possibly incorrect [bug]
|
||||
- #1275 WithMappingFromOpenApiFile - Support for OpenAPI 3.1.0 [feature]
|
||||
- #1276 Add WireMock Inspector command to Aspire integration [feature]
|
||||
- #1277 Newtonsoft.Json.JsonReaderException: 'Unable to read the JSON string.' when using IStringMatcher with an empty body [bug]
|
||||
- #1284 WireMock.Net.AspNetCore.Middleware does not work in hosted services [bug]
|
||||
|
||||
The full release notes can be found here: https://github.com/WireMock-Net/WireMock.Net/blob/master/CHANGELOG.md
|
||||
12
README.md
12
README.md
@@ -45,6 +45,7 @@ For more info, see also this WIKI page: [What is WireMock.Net](https://github.co
|
||||
| **WireMock.Net.Aspire** | [](https://www.nuget.org/packages/WireMock.Net.Aspire) | [](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.Aspire)
|
||||
| **WireMock.Net.AspNetCore.Middleware** | [](https://www.nuget.org/packages/WireMock.Net.AspNetCore.Middleware) | [](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.AspNetCore.Middleware)
|
||||
| | | |
|
||||
| **WireMock.Net.AwesomeAssertions** | [](https://www.nuget.org/packages/WireMock.Net.AwesomeAssertions) | [](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.AwesomeAssertions)
|
||||
| **WireMock.Net.FluentAssertions** | [](https://www.nuget.org/packages/WireMock.Net.FluentAssertions) | [](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.FluentAssertions)
|
||||
| **WireMock.Net.xUnit** | [](https://www.nuget.org/packages/WireMock.Net.xUnit) | [](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.xUnit)
|
||||
| **WireMock.Net.TUnit** | [](https://www.nuget.org/packages/WireMock.Net.TUnit) | [](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.TUnit)
|
||||
@@ -64,6 +65,17 @@ A breaking change is introduced which is related to System.Linq.Dynamic.Core Dyn
|
||||
- The `LinqMatcher` is not allowed.
|
||||
- The [Handlebars.Net.Helpers.DynamicLinq](https://www.nuget.org/packages/Handlebars.Net.Helpers.DynamicLinq) package is not included anymore.
|
||||
|
||||
### 1.8.0
|
||||
Some breaking changes are introduced in this version:
|
||||
|
||||
#### Handlebars.Net `File`-helper
|
||||
By default, the internal Handlebars.Net `File`-helper is not allowed anymore because of potential security issues.
|
||||
To still enable this feature, you need to set the `AllowedCustomHandlebarHelpers` property to `File` in the `HandlebarsSettings` property in `WireMockServerSettings`.
|
||||
|
||||
#### Handlebars.Net `Environment`-helper
|
||||
By default, the Handlebars.Net `Environment`-helper is not automatically allowed anymore because of potential security issues.
|
||||
To still enable this feature, you need to add the `Environment` category to the `AllowedHandlebarsHelpers` list-property in the `HandlebarsSettings` property in `WireMockServerSettings`.
|
||||
|
||||
---
|
||||
|
||||
## :memo: Development
|
||||
|
||||
@@ -128,6 +128,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.TestWebApplica
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.Middleware.Tests", "test\WireMock.Net.Middleware.Tests\WireMock.Net.Middleware.Tests.csproj", "{A5FEF4F7-7DA2-4962-89A8-16BA942886E5}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.AwesomeAssertions", "src\WireMock.Net.AwesomeAssertions\WireMock.Net.AwesomeAssertions.csproj", "{7753670F-7C7F-44BF-8BC7-08325588E60C}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -302,6 +304,10 @@ Global
|
||||
{A5FEF4F7-7DA2-4962-89A8-16BA942886E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A5FEF4F7-7DA2-4962-89A8-16BA942886E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A5FEF4F7-7DA2-4962-89A8-16BA942886E5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7753670F-7C7F-44BF-8BC7-08325588E60C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7753670F-7C7F-44BF-8BC7-08325588E60C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7753670F-7C7F-44BF-8BC7-08325588E60C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7753670F-7C7F-44BF-8BC7-08325588E60C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -351,6 +357,7 @@ Global
|
||||
{B6269AAC-170A-4346-8B9A-579DED3D9A13} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
|
||||
{6B30AA9F-DA04-4EB5-B03C-45A8EF272ECE} = {0BB8B634-407A-4610-A91F-11586990767A}
|
||||
{A5FEF4F7-7DA2-4962-89A8-16BA942886E5} = {0BB8B634-407A-4610-A91F-11586990767A}
|
||||
{7753670F-7C7F-44BF-8BC7-08325588E60C} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {DC539027-9852-430C-B19F-FD035D018458}
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Jmes/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Levenstein/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=openapi/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=opentelemetry/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Pacticipant/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=protobuf/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Raml/@EntryIndexedValue">True</s:Boolean>
|
||||
|
||||
@@ -1,181 +1,176 @@
|
||||
variables:
|
||||
Prerelease: 'ci'
|
||||
buildId: "1$(Build.BuildId)"
|
||||
buildProjects: '**/src/**/*.csproj'
|
||||
|
||||
jobs:
|
||||
- job: Linux_Build_Test_SonarCloud
|
||||
|
||||
pool:
|
||||
vmImage: 'Ubuntu-latest'
|
||||
|
||||
steps:
|
||||
- script: |
|
||||
echo "BuildId = $(buildId)"
|
||||
displayName: 'Print buildId'
|
||||
|
||||
- task: CmdLine@2
|
||||
displayName: 'Install .NET Aspire workload'
|
||||
inputs:
|
||||
script: 'dotnet workload install aspire'
|
||||
|
||||
- script: |
|
||||
dotnet tool install --global dotnet-sonarscanner
|
||||
dotnet tool install --global dotnet-coverage
|
||||
displayName: 'Install dotnet tools'
|
||||
|
||||
- task: PowerShell@2
|
||||
displayName: "Use JDK17 by default"
|
||||
inputs:
|
||||
targetType: 'inline'
|
||||
script: |
|
||||
$jdkPath = $env:JAVA_HOME_17_X64
|
||||
Write-Host "##vso[task.setvariable variable=JAVA_HOME]$jdkPath"
|
||||
|
||||
- script: |
|
||||
dotnet dev-certs https --trust || true
|
||||
displayName: 'dotnet dev-certs https'
|
||||
|
||||
# See: https://docs.sonarsource.com/sonarcloud/enriching/test-coverage/dotnet-test-coverage
|
||||
- script: |
|
||||
dotnet sonarscanner begin /k:"WireMock-Net_WireMock.Net" /o:"wiremock-net" /d:sonar.branch.name=$(Build.SourceBranchName) /d:sonar.host.url="https://sonarcloud.io" /d:sonar.token="$(SONAR_TOKEN)" /d:sonar.pullrequest.provider=github /d:sonar.cs.vscoveragexml.reportsPaths=**/wiremock-coverage-*.xml /d:sonar.verbose=true
|
||||
displayName: 'Begin analysis on SonarCloud'
|
||||
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Build Unit tests'
|
||||
inputs:
|
||||
command: 'build'
|
||||
projects: '**/test/**/*.csproj'
|
||||
arguments: '--configuration Debug --framework net8.0'
|
||||
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
dotnet-coverage collect "dotnet test ./test/WireMock.Net.Tests/WireMock.Net.Tests.csproj --configuration Debug --no-build --framework net8.0" -f xml -o "wiremock-coverage-xunit.xml"
|
||||
displayName: 'WireMock.Net.Tests with Coverage'
|
||||
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
dotnet-coverage collect "dotnet test ./test/WireMock.Net.TUnitTests/WireMock.Net.TUnitTests.csproj --configuration Debug --no-build --framework net8.0" -f xml -o "wiremock-coverage-tunit.xml"
|
||||
displayName: 'WireMock.Net.TUnitTests with Coverage'
|
||||
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
dotnet-coverage collect "dotnet test ./test/WireMock.Net.Middleware.Tests/WireMock.Net.Middleware.Tests.csproj --configuration Debug --no-build --framework net8.0" -f xml -o "wiremock-coverage-middleware.xml"
|
||||
displayName: 'WireMock.Net.Middleware.Tests with Coverage'
|
||||
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
dotnet-coverage collect "dotnet test ./test/WireMock.Net.Aspire.Tests/WireMock.Net.Aspire.Tests.csproj --configuration Debug --no-build" -f xml -o "wiremock-coverage-aspire.xml"
|
||||
displayName: 'WireMock.Net.Aspire.Tests with Coverage'
|
||||
|
||||
- task: CmdLine@2
|
||||
displayName: 'Merge coverage files'
|
||||
inputs:
|
||||
script: 'dotnet coverage merge **/wiremock-coverage-*.xml --output ./test/wiremock-coverage.xml --output-format xml'
|
||||
|
||||
- script: |
|
||||
dotnet sonarscanner end /d:sonar.token="$(SONAR_TOKEN)"
|
||||
displayName: 'End analysis on SonarCloud'
|
||||
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests
|
||||
|
||||
- task: whitesource.ws-bolt.bolt.wss.WhiteSource Bolt@19
|
||||
displayName: 'WhiteSource Bolt'
|
||||
condition: and(succeeded(), eq(variables['RUN_WHITESOURCE'], 'yes'))
|
||||
|
||||
- script: |
|
||||
bash <(curl https://codecov.io/bash) -t $(CODECOV_TOKEN) -f ./test/wiremock-coverage.xml
|
||||
displayName: 'Upload coverage results to codecov'
|
||||
|
||||
- task: PublishTestResults@2
|
||||
condition: and(succeeded(), eq(variables['PUBLISH_TESTRESULTS'], 'yes'))
|
||||
inputs:
|
||||
testRunner: VSTest
|
||||
testResultsFiles: '**/*.trx'
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: Publish coverage files
|
||||
inputs:
|
||||
PathtoPublish: './test/WireMock.Net.Tests/coverage.net8.0.opencover.xml'
|
||||
|
||||
- job: Windows_Build_Test
|
||||
|
||||
pool:
|
||||
vmImage: 'windows-2022'
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET 8.0
|
||||
inputs:
|
||||
packageType: 'sdk'
|
||||
version: '8.0.x'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'WireMock.Net.Tests with Coverage'
|
||||
inputs:
|
||||
command: 'test'
|
||||
projects: './test/WireMock.Net.Tests/WireMock.Net.Tests.csproj'
|
||||
arguments: '--configuration Debug --framework net8.0 --collect:"XPlat Code Coverage" --logger trx'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'WireMock.Net.TUnitTests with Coverage'
|
||||
inputs:
|
||||
command: 'test'
|
||||
projects: './test/WireMock.Net.TUnitTests/WireMock.Net.TUnitTests.csproj'
|
||||
arguments: '--configuration Debug --framework net8.0 --collect:"XPlat Code Coverage" --logger trx'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'WireMock.Net.Middleware.Tests with Coverage'
|
||||
inputs:
|
||||
command: 'test'
|
||||
projects: './test/WireMock.Net.Middleware.Tests/WireMock.Net.Middleware.Tests.csproj'
|
||||
arguments: '--configuration Debug --framework net8.0 --collect:"XPlat Code Coverage" --logger trx'
|
||||
|
||||
- job: Windows_Release_to_MyGet
|
||||
dependsOn: Windows_Build_Test
|
||||
|
||||
pool:
|
||||
vmImage: 'windows-2022'
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET 8.0
|
||||
inputs:
|
||||
packageType: 'sdk'
|
||||
version: '8.0.x'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Build Release
|
||||
inputs:
|
||||
command: 'build'
|
||||
arguments: /p:Configuration=Release
|
||||
projects: $(buildProjects)
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Pack
|
||||
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests
|
||||
inputs:
|
||||
command: pack
|
||||
configuration: 'Release'
|
||||
packagesToPack: $(buildProjects)
|
||||
nobuild: true
|
||||
packDirectory: '$(Build.ArtifactStagingDirectory)/packages'
|
||||
verbosityPack: 'normal'
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: Publish Artifacts
|
||||
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests
|
||||
inputs:
|
||||
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Push to MyGet
|
||||
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests
|
||||
inputs:
|
||||
command: custom
|
||||
custom: nuget
|
||||
variables:
|
||||
Prerelease: 'ci'
|
||||
buildId: "1$(Build.BuildId)"
|
||||
buildProjects: '**/src/**/*.csproj'
|
||||
|
||||
jobs:
|
||||
- job: Linux_Build_Test_SonarCloud
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-22.04'
|
||||
|
||||
steps:
|
||||
- script: |
|
||||
echo "BuildId = $(buildId)"
|
||||
displayName: 'Print buildId'
|
||||
|
||||
- script: |
|
||||
dotnet tool install --global dotnet-sonarscanner
|
||||
dotnet tool install --global dotnet-coverage
|
||||
displayName: 'Install dotnet tools'
|
||||
|
||||
- task: PowerShell@2
|
||||
displayName: "Use JDK17 by default"
|
||||
inputs:
|
||||
targetType: 'inline'
|
||||
script: |
|
||||
$jdkPath = $env:JAVA_HOME_17_X64
|
||||
Write-Host "##vso[task.setvariable variable=JAVA_HOME]$jdkPath"
|
||||
|
||||
- script: |
|
||||
dotnet dev-certs https --trust || true
|
||||
displayName: 'dotnet dev-certs https'
|
||||
|
||||
# See: https://docs.sonarsource.com/sonarcloud/enriching/test-coverage/dotnet-test-coverage
|
||||
- script: |
|
||||
dotnet sonarscanner begin /k:"WireMock-Net_WireMock.Net" /o:"wiremock-net" /d:sonar.branch.name=$(Build.SourceBranchName) /d:sonar.host.url="https://sonarcloud.io" /d:sonar.token="$(SONAR_TOKEN)" /d:sonar.pullrequest.provider=github /d:sonar.cs.vscoveragexml.reportsPaths=**/wiremock-coverage-*.xml /d:sonar.verbose=true
|
||||
displayName: 'Begin analysis on SonarCloud'
|
||||
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Build Unit tests'
|
||||
inputs:
|
||||
command: 'build'
|
||||
projects: '**/test/**/*.csproj'
|
||||
arguments: '--configuration Debug --framework net8.0'
|
||||
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
dotnet-coverage collect "dotnet test ./test/WireMock.Net.Tests/WireMock.Net.Tests.csproj --configuration Debug --no-build --framework net8.0" -f xml -o "wiremock-coverage-xunit.xml"
|
||||
displayName: 'WireMock.Net.Tests with Coverage'
|
||||
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
dotnet-coverage collect "dotnet test ./test/WireMock.Net.TUnitTests/WireMock.Net.TUnitTests.csproj --configuration Debug --no-build --framework net8.0" -f xml -o "wiremock-coverage-tunit.xml"
|
||||
displayName: 'WireMock.Net.TUnitTests with Coverage'
|
||||
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
dotnet-coverage collect "dotnet test ./test/WireMock.Net.Middleware.Tests/WireMock.Net.Middleware.Tests.csproj --configuration Debug --no-build --framework net8.0" -f xml -o "wiremock-coverage-middleware.xml"
|
||||
displayName: 'WireMock.Net.Middleware.Tests with Coverage'
|
||||
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
dotnet-coverage collect "dotnet test ./test/WireMock.Net.Aspire.Tests/WireMock.Net.Aspire.Tests.csproj --configuration Debug --no-build" -f xml -o "wiremock-coverage-aspire.xml"
|
||||
displayName: 'WireMock.Net.Aspire.Tests with Coverage'
|
||||
|
||||
- task: CmdLine@2
|
||||
displayName: 'Merge coverage files'
|
||||
inputs:
|
||||
script: 'dotnet coverage merge **/wiremock-coverage-*.xml --output ./test/wiremock-coverage.xml --output-format xml'
|
||||
|
||||
- script: |
|
||||
dotnet sonarscanner end /d:sonar.token="$(SONAR_TOKEN)"
|
||||
displayName: 'End analysis on SonarCloud'
|
||||
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests
|
||||
|
||||
- task: whitesource.ws-bolt.bolt.wss.WhiteSource Bolt@19
|
||||
displayName: 'WhiteSource Bolt'
|
||||
condition: and(succeeded(), eq(variables['RUN_WHITESOURCE'], 'yes'))
|
||||
|
||||
- script: |
|
||||
bash <(curl https://codecov.io/bash) -t $(CODECOV_TOKEN) -f ./test/wiremock-coverage.xml
|
||||
displayName: 'Upload coverage results to codecov'
|
||||
|
||||
- task: PublishTestResults@2
|
||||
condition: and(succeeded(), eq(variables['PUBLISH_TESTRESULTS'], 'yes'))
|
||||
inputs:
|
||||
testRunner: VSTest
|
||||
testResultsFiles: '**/*.trx'
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: Publish coverage files
|
||||
inputs:
|
||||
PathtoPublish: './test/WireMock.Net.Tests/coverage.net8.0.opencover.xml'
|
||||
|
||||
- job: Windows_Build_Test
|
||||
|
||||
pool:
|
||||
vmImage: 'windows-2022'
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET 8.0
|
||||
inputs:
|
||||
packageType: 'sdk'
|
||||
version: '8.0.x'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'WireMock.Net.Tests with Coverage'
|
||||
inputs:
|
||||
command: 'test'
|
||||
projects: './test/WireMock.Net.Tests/WireMock.Net.Tests.csproj'
|
||||
arguments: '--configuration Debug --framework net8.0 --collect:"XPlat Code Coverage" --logger trx'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'WireMock.Net.TUnitTests with Coverage'
|
||||
inputs:
|
||||
command: 'test'
|
||||
projects: './test/WireMock.Net.TUnitTests/WireMock.Net.TUnitTests.csproj'
|
||||
arguments: '--configuration Debug --framework net8.0 --collect:"XPlat Code Coverage" --logger trx'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'WireMock.Net.Middleware.Tests with Coverage'
|
||||
inputs:
|
||||
command: 'test'
|
||||
projects: './test/WireMock.Net.Middleware.Tests/WireMock.Net.Middleware.Tests.csproj'
|
||||
arguments: '--configuration Debug --framework net8.0 --collect:"XPlat Code Coverage" --logger trx'
|
||||
|
||||
- job: Windows_Release_to_MyGet
|
||||
dependsOn: Windows_Build_Test
|
||||
|
||||
pool:
|
||||
vmImage: 'windows-2022'
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET 8.0
|
||||
inputs:
|
||||
packageType: 'sdk'
|
||||
version: '8.0.x'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Build Release
|
||||
inputs:
|
||||
command: 'build'
|
||||
arguments: /p:Configuration=Release
|
||||
projects: $(buildProjects)
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Pack
|
||||
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests
|
||||
inputs:
|
||||
command: pack
|
||||
configuration: 'Release'
|
||||
packagesToPack: $(buildProjects)
|
||||
nobuild: true
|
||||
packDirectory: '$(Build.ArtifactStagingDirectory)/packages'
|
||||
verbosityPack: 'normal'
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: Publish Artifacts
|
||||
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests
|
||||
inputs:
|
||||
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Push to MyGet
|
||||
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests
|
||||
inputs:
|
||||
command: custom
|
||||
custom: nuget
|
||||
arguments: push $(Build.ArtifactStagingDirectory)\packages\*.nupkg -n -s https://www.myget.org/F/wiremock-net/api/v3/index.json -k $(MyGetKey)
|
||||
@@ -1,23 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsAspireHost>true</IsAspireHost>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AspireApp1.ApiService\AspireApp1.ApiService.csproj" />
|
||||
<ProjectReference Include="..\AspireApp1.Web\AspireApp1.Web.csproj" />
|
||||
|
||||
<!-- https://learn.microsoft.com/en-us/dotnet/aspire/extensibility/custom-resources?tabs=windows#create-library-for-resource-extension -->
|
||||
<ProjectReference Include="..\..\src\WireMock.Net.Aspire\WireMock.Net.Aspire.csproj" IsAspireProjectResource="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Hosting.AppHost" Version="8.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Sdk Name="Aspire.AppHost.Sdk" Version="9.2.0" />
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AspireApp1.ApiService\AspireApp1.ApiService.csproj" />
|
||||
<ProjectReference Include="..\AspireApp1.Web\AspireApp1.Web.csproj" />
|
||||
|
||||
<!-- https://learn.microsoft.com/en-us/dotnet/aspire/extensibility/custom-resources?tabs=windows#create-library-for-resource-extension -->
|
||||
<ProjectReference Include="..\..\src\WireMock.Net.Aspire\WireMock.Net.Aspire.csproj" IsAspireProjectResource="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsAspireHost>true</IsAspireHost>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AspireApp1.ApiService\AspireApp1.ApiService.csproj" />
|
||||
<ProjectReference Include="..\AspireApp1.Web\AspireApp1.Web.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Hosting.AppHost" Version="8.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Sdk Name="Aspire.AppHost.Sdk" Version="9.2.0" />
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AspireApp1.ApiService\AspireApp1.ApiService.csproj" />
|
||||
<ProjectReference Include="..\AspireApp1.Web\AspireApp1.Web.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -97,6 +97,46 @@ message HelloReply {
|
||||
fullName:String
|
||||
}";
|
||||
|
||||
private static void RunSse()
|
||||
{
|
||||
var server = WireMockServer.Start(new WireMockServerSettings
|
||||
{
|
||||
Port = 9091,
|
||||
StartAdminInterface = true,
|
||||
Logger = new WireMockConsoleLogger()
|
||||
});
|
||||
server
|
||||
.WhenRequest(r => r
|
||||
.UsingGet()
|
||||
.WithPath("/sse")
|
||||
)
|
||||
.ThenRespondWith(r => r
|
||||
.WithHeader("Content-Type", "text/event-stream")
|
||||
.WithHeader("Cache-Control", "no-cache")
|
||||
.WithHeader("Connection", "keep-alive")
|
||||
.WithSseBody(async (_, q) =>
|
||||
{
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
q.Write("test " + i + "\r\n");
|
||||
await Task.Delay(5000);
|
||||
}
|
||||
|
||||
q.Close();
|
||||
})
|
||||
);
|
||||
|
||||
server
|
||||
.WhenRequest(r => r
|
||||
.UsingGet()
|
||||
)
|
||||
.ThenRespondWith(r => r
|
||||
.WithBody("normal")
|
||||
);
|
||||
|
||||
System.Console.ReadKey();
|
||||
}
|
||||
|
||||
private static void RunOnLocal()
|
||||
{
|
||||
try
|
||||
@@ -136,6 +176,7 @@ message HelloReply {
|
||||
|
||||
public static void Run()
|
||||
{
|
||||
RunSse();
|
||||
RunOnLocal();
|
||||
|
||||
var mappingBuilder = new MappingBuilder();
|
||||
|
||||
30
src/WireMock.Net.Abstractions/Models/IBlockingQueue.cs
Normal file
30
src/WireMock.Net.Abstractions/Models/IBlockingQueue.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace WireMock.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A simple implementation for a Blocking Queue.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Specifies the type of elements in the queue.</typeparam>
|
||||
public interface IBlockingQueue<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes an item to the queue and signals that an item is available.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to be added to the queue.</param>
|
||||
void Write(T item);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to read an item from the queue. Waits until an item is available or the timeout occurs.
|
||||
/// </summary>
|
||||
/// <param name="item">The item read from the queue, or default if the timeout occurs.</param>
|
||||
/// <returns>True if an item was successfully read; otherwise, false.</returns>
|
||||
bool TryRead([NotNullWhen(true)] out T? item);
|
||||
|
||||
/// <summary>
|
||||
/// Closes the queue and signals all waiting threads.
|
||||
/// </summary>
|
||||
public void Close();
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using WireMock.Models;
|
||||
using WireMock.Types;
|
||||
|
||||
@@ -71,7 +72,7 @@ public interface IBodyData
|
||||
Encoding? Encoding { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Defines if this BodyData is the result of a dynamically created response-string. (
|
||||
/// Defines if this BodyData is the result of a dynamically created response-string.
|
||||
/// </summary>
|
||||
public string? IsFuncUsed { get; set; }
|
||||
|
||||
@@ -86,4 +87,14 @@ public interface IBodyData
|
||||
/// </summary>
|
||||
public string? ProtoBufMessageType { get; set; }
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Defines the queue to use for Server-Sent Events (string).
|
||||
/// </summary>
|
||||
public IBlockingQueue<string?>? SseStringQueue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Defines if the body is using Server-Sent Events (string).
|
||||
/// </summary>
|
||||
public Task? BodyAsSseStringTask { get; set; }
|
||||
}
|
||||
@@ -45,5 +45,10 @@ public enum BodyType
|
||||
/// <summary>
|
||||
/// Body is a ProtoBuf Byte array
|
||||
/// </summary>
|
||||
ProtoBuf
|
||||
ProtoBuf,
|
||||
|
||||
/// <summary>
|
||||
/// Use Server-Sent Events (string)
|
||||
/// </summary>
|
||||
SseString
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
|
||||
namespace WireMock.Types;
|
||||
|
||||
/// <summary>
|
||||
/// A enum defining the supported Handlebar helpers.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum CustomHandlebarsHelpers
|
||||
{
|
||||
None = 0,
|
||||
|
||||
File = 1,
|
||||
|
||||
All = File
|
||||
}
|
||||
@@ -61,4 +61,11 @@
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Nullable" Version="1.3.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -36,7 +36,6 @@ internal class WireMockDelegationHandler : DelegatingHandler
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
Guard.NotNull(request);
|
||||
Guard.NotNull(_httpContextAccessor.HttpContext);
|
||||
|
||||
if (_settings.AlwaysRedirect || IsWireMockRedirectHeaderSetToTrue())
|
||||
{
|
||||
@@ -57,16 +56,30 @@ internal class WireMockDelegationHandler : DelegatingHandler
|
||||
|
||||
private bool IsWireMockRedirectHeaderSetToTrue()
|
||||
{
|
||||
var httpContext = _httpContextAccessor.HttpContext;
|
||||
if (httpContext is null)
|
||||
{
|
||||
_logger.LogDebug("HttpContext is not available in current runtime environment");
|
||||
return false;
|
||||
}
|
||||
|
||||
return
|
||||
_httpContextAccessor.HttpContext!.Request.Headers.TryGetValue(AppConstants.HEADER_REDIRECT, out var values) &&
|
||||
httpContext.Request.Headers.TryGetValue(AppConstants.HEADER_REDIRECT, out var values) &&
|
||||
bool.TryParse(values.ToString(), out var shouldRedirectToWireMock) && shouldRedirectToWireMock;
|
||||
}
|
||||
|
||||
private bool TryGetDelayHeaderValue(out int delayInMs)
|
||||
{
|
||||
delayInMs = 0;
|
||||
var httpContext = _httpContextAccessor.HttpContext;
|
||||
if (httpContext is null)
|
||||
{
|
||||
_logger.LogDebug("HttpContext is not available in current runtime environment");
|
||||
return false;
|
||||
}
|
||||
|
||||
return
|
||||
_httpContextAccessor.HttpContext!.Request.Headers.TryGetValue(AppConstants.HEADER_RESPONSE_DELAY, out var values) &&
|
||||
httpContext.Request.Headers.TryGetValue(AppConstants.HEADER_RESPONSE_DELAY, out var values) &&
|
||||
int.TryParse(values.ToString(), out delayInMs);
|
||||
}
|
||||
}
|
||||
@@ -1,49 +1,49 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Description>Aspire extension to start a WireMock.Net server to stub an api.</Description>
|
||||
<AssemblyTitle>WireMock.Net.Aspire</AssemblyTitle>
|
||||
<Authors>Stef Heyenrath</Authors>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<AssemblyName>WireMock.Net.Aspire</AssemblyName>
|
||||
<PackageId>WireMock.Net.Aspire</PackageId>
|
||||
<PackageTags>dotnet;aspire;wiremock;extension</PackageTags>
|
||||
<ProjectGuid>{B6269AAC-170A-4346-8B9A-579DED3D9A12}</ProjectGuid>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
|
||||
<CodeAnalysisRuleSet>../WireMock.Net/WireMock.Net.ruleset</CodeAnalysisRuleSet>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>../WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
|
||||
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageIcon>WireMock.Net-LogoAspire.png</PackageIcon>
|
||||
<ApplicationIcon>../../resources/WireMock.Net-LogoAspire.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="../../resources/WireMock.Net-Logo.png" />
|
||||
<None Include="../../resources/WireMock.Net-LogoAspire.png" Pack="true" PackagePath="" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\WireMock.Net\Util\EnhancedFileSystemWatcher.cs" Link="Utils\EnhancedFileSystemWatcher.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Hosting" Version="8.2.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\WireMock.Net.RestClient\WireMock.Net.RestClient.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Description>Aspire extension to start a WireMock.Net server to stub an api.</Description>
|
||||
<AssemblyTitle>WireMock.Net.Aspire</AssemblyTitle>
|
||||
<Authors>Stef Heyenrath</Authors>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<AssemblyName>WireMock.Net.Aspire</AssemblyName>
|
||||
<PackageId>WireMock.Net.Aspire</PackageId>
|
||||
<PackageTags>dotnet;aspire;wiremock;extension</PackageTags>
|
||||
<ProjectGuid>{B6269AAC-170A-4346-8B9A-579DED3D9A12}</ProjectGuid>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
|
||||
<CodeAnalysisRuleSet>../WireMock.Net/WireMock.Net.ruleset</CodeAnalysisRuleSet>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>../WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
|
||||
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageIcon>WireMock.Net-LogoAspire.png</PackageIcon>
|
||||
<ApplicationIcon>../../resources/WireMock.Net-LogoAspire.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="../../resources/WireMock.Net-Logo.png" />
|
||||
<None Include="../../resources/WireMock.Net-LogoAspire.png" Pack="true" PackagePath="" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\WireMock.Net\Util\EnhancedFileSystemWatcher.cs" Link="Utils\EnhancedFileSystemWatcher.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Hosting" Version="9.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\WireMock.Net.RestClient\WireMock.Net.RestClient.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
43
src/WireMock.Net.Aspire/WireMockInspector.cs
Normal file
43
src/WireMock.Net.Aspire/WireMockInspector.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Aspire.Hosting.WireMock;
|
||||
|
||||
internal static class WireMockInspector
|
||||
{
|
||||
/// <summary>
|
||||
/// Opens the WireMockInspector tool to inspect the WireMock server.
|
||||
/// </summary>
|
||||
/// <param name="wireMockUrl"></param>
|
||||
/// <param name="title"></param>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
/// <remarks>
|
||||
/// Copy of <see href="https://github.com/WireMock-Net/WireMockInspector/blob/main/src/WireMock.Net.Extensions.WireMockInspector/WireMockServerExtensions.cs" />
|
||||
/// without requestFilters and no call to WaitForExit() method in the process so it doesn't block the caller.
|
||||
/// </remarks>
|
||||
public static void Inspect(string wireMockUrl, [CallerMemberName] string title = "")
|
||||
{
|
||||
try
|
||||
{
|
||||
var arguments = $"attach --adminUrl {wireMockUrl} --autoLoad --instanceName \"{title}\"";
|
||||
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "wiremockinspector",
|
||||
Arguments = arguments,
|
||||
UseShellExecute = false
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new InvalidOperationException
|
||||
(
|
||||
message: @"Cannot find installation of WireMockInspector.
|
||||
Execute the following command to install WireMockInspector dotnet tool:
|
||||
> dotnet tool install WireMockInspector --global --no-cache --ignore-failed-sources
|
||||
To get more info please visit https://github.com/WireMock-Net/WireMockInspector",
|
||||
innerException: e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,175 +1,222 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using Aspire.Hosting.ApplicationModel;
|
||||
using Aspire.Hosting.Lifecycle;
|
||||
using Stef.Validation;
|
||||
using WireMock.Client.Builders;
|
||||
using WireMock.Net.Aspire;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Aspire.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for adding WireMock.Net Server resources to the application model.
|
||||
/// </summary>
|
||||
public static class WireMockServerBuilderExtensions
|
||||
{
|
||||
// Linux only (https://github.com/dotnet/aspire/issues/854)
|
||||
private const string DefaultLinuxImage = "sheyenrath/wiremock.net-alpine";
|
||||
private const string DefaultLinuxMappingsPath = "/app/__admin/mappings";
|
||||
|
||||
/// <summary>
|
||||
/// Adds a WireMock.Net Server resource to the application model.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
|
||||
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency.</param>
|
||||
/// <param name="port">The HTTP port for the WireMock Server.</param>
|
||||
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
|
||||
public static IResourceBuilder<WireMockServerResource> AddWireMock(this IDistributedApplicationBuilder builder, string name, int? port = null)
|
||||
{
|
||||
Guard.NotNull(builder);
|
||||
Guard.NotNullOrWhiteSpace(name);
|
||||
Guard.Condition(port, p => p is null or > 0 and <= ushort.MaxValue);
|
||||
|
||||
return builder.AddWireMock(name, callback =>
|
||||
{
|
||||
callback.HttpPort = port;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a WireMock.Net Server resource to the application model.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
|
||||
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency.</param>
|
||||
/// <param name="arguments">The arguments to start the WireMock.Net Server.</param>
|
||||
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
|
||||
public static IResourceBuilder<WireMockServerResource> AddWireMock(this IDistributedApplicationBuilder builder, string name, WireMockServerArguments arguments)
|
||||
{
|
||||
Guard.NotNull(builder);
|
||||
Guard.NotNullOrWhiteSpace(name);
|
||||
Guard.NotNull(arguments);
|
||||
|
||||
var wireMockContainerResource = new WireMockServerResource(name, arguments);
|
||||
var resourceBuilder = builder
|
||||
.AddResource(wireMockContainerResource)
|
||||
.WithImage(DefaultLinuxImage)
|
||||
.WithEnvironment(ctx => ctx.EnvironmentVariables.Add("DOTNET_USE_POLLING_FILE_WATCHER", "1")) // https://khalidabuhakmeh.com/aspnet-docker-gotchas-and-workarounds#configuration-reloads-and-filesystemwatcher
|
||||
.WithHttpEndpoint(port: arguments.HttpPort, targetPort: WireMockServerArguments.HttpContainerPort);
|
||||
|
||||
if (!string.IsNullOrEmpty(arguments.MappingsPath))
|
||||
{
|
||||
resourceBuilder = resourceBuilder.WithBindMount(arguments.MappingsPath, DefaultLinuxMappingsPath);
|
||||
}
|
||||
|
||||
resourceBuilder = resourceBuilder.WithArgs(ctx =>
|
||||
{
|
||||
foreach (var arg in arguments.GetArgs())
|
||||
{
|
||||
ctx.Args.Add(arg);
|
||||
}
|
||||
});
|
||||
|
||||
return resourceBuilder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a WireMock.Net Server resource to the application model.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
|
||||
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency.</param>
|
||||
/// <param name="callback">A callback that allows for setting the <see cref="WireMockServerArguments"/>.</param>
|
||||
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
|
||||
public static IResourceBuilder<WireMockServerResource> AddWireMock(this IDistributedApplicationBuilder builder, string name, Action<WireMockServerArguments> callback)
|
||||
{
|
||||
Guard.NotNull(builder);
|
||||
Guard.NotNullOrWhiteSpace(name);
|
||||
Guard.NotNull(callback);
|
||||
|
||||
var arguments = new WireMockServerArguments();
|
||||
callback(arguments);
|
||||
|
||||
return builder.AddWireMock(name, arguments);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines if the static mappings should be read at startup.
|
||||
///
|
||||
/// Default set to <c>false</c>.
|
||||
/// </summary>
|
||||
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
|
||||
public static IResourceBuilder<WireMockServerResource> WithReadStaticMappings(this IResourceBuilder<WireMockServerResource> wiremock)
|
||||
{
|
||||
Guard.NotNull(wiremock).Resource.Arguments.ReadStaticMappings = true;
|
||||
return wiremock;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Watch the static mapping files + folder for changes when running.
|
||||
///
|
||||
/// Default set to <c>false</c>.
|
||||
/// </summary>
|
||||
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
|
||||
public static IResourceBuilder<WireMockServerResource> WithWatchStaticMappings(this IResourceBuilder<WireMockServerResource> wiremock)
|
||||
{
|
||||
Guard.NotNull(wiremock).Resource.Arguments.WatchStaticMappings = true;
|
||||
return wiremock;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the path for the (static) mapping json files.
|
||||
/// </summary>
|
||||
/// <param name="wiremock">The <see cref="IResourceBuilder{WireMockServerResource}"/>.</param>
|
||||
/// <param name="mappingsPath">The local path.</param>
|
||||
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
|
||||
public static IResourceBuilder<WireMockServerResource> WithMappingsPath(this IResourceBuilder<WireMockServerResource> wiremock, string mappingsPath)
|
||||
{
|
||||
Guard.NotNullOrWhiteSpace(mappingsPath);
|
||||
Guard.NotNull(wiremock).Resource.Arguments.MappingsPath = mappingsPath;
|
||||
|
||||
return wiremock.WithBindMount(mappingsPath, DefaultLinuxMappingsPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the admin username and password for accessing the admin interface from WireMock.Net via HTTP.
|
||||
/// </summary>
|
||||
/// <param name="wiremock">The <see cref="IResourceBuilder{WireMockServerResource}"/>.</param>
|
||||
/// <param name="username">The admin username.</param>
|
||||
/// <param name="password">The admin password.</param>
|
||||
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
|
||||
public static IResourceBuilder<WireMockServerResource> WithAdminUserNameAndPassword(this IResourceBuilder<WireMockServerResource> wiremock, string username, string password)
|
||||
{
|
||||
Guard.NotNull(wiremock);
|
||||
|
||||
wiremock.Resource.Arguments.AdminUsername = Guard.NotNull(username);
|
||||
wiremock.Resource.Arguments.AdminPassword = Guard.NotNull(password);
|
||||
return wiremock;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use WireMock Client's AdminApiMappingBuilder to configure the WireMock.Net resource.
|
||||
/// </summary>
|
||||
/// <param name="wiremock">The <see cref="IResourceBuilder{WireMockServerResource}"/>.</param>
|
||||
/// <param name="configure">Delegate that will be invoked to configure the WireMock.Net resource.</param>
|
||||
/// <returns></returns>
|
||||
public static IResourceBuilder<WireMockServerResource> WithApiMappingBuilder(this IResourceBuilder<WireMockServerResource> wiremock, Func<AdminApiMappingBuilder, Task> configure)
|
||||
{
|
||||
return wiremock.WithApiMappingBuilder((adminApiMappingBuilder, _) => configure.Invoke(adminApiMappingBuilder));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use WireMock Client's AdminApiMappingBuilder to configure the WireMock.Net resource.
|
||||
/// </summary>
|
||||
/// <param name="wiremock">The <see cref="IResourceBuilder{WireMockServerResource}"/>.</param>
|
||||
/// <param name="configure">Delegate that will be invoked to configure the WireMock.Net resource.</param>
|
||||
/// <returns></returns>
|
||||
public static IResourceBuilder<WireMockServerResource> WithApiMappingBuilder(this IResourceBuilder<WireMockServerResource> wiremock, Func<AdminApiMappingBuilder, CancellationToken, Task> configure)
|
||||
{
|
||||
Guard.NotNull(wiremock);
|
||||
|
||||
wiremock.ApplicationBuilder.Services.TryAddLifecycleHook<WireMockServerLifecycleHook>();
|
||||
wiremock.Resource.Arguments.ApiMappingBuilder = configure;
|
||||
|
||||
return wiremock;
|
||||
}
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using Aspire.Hosting.ApplicationModel;
|
||||
using Aspire.Hosting.Lifecycle;
|
||||
using Aspire.Hosting.WireMock;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Stef.Validation;
|
||||
using WireMock.Client.Builders;
|
||||
using WireMock.Net.Aspire;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Aspire.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for adding WireMock.Net Server resources to the application model.
|
||||
/// </summary>
|
||||
public static class WireMockServerBuilderExtensions
|
||||
{
|
||||
// Linux only (https://github.com/dotnet/aspire/issues/854)
|
||||
private const string DefaultLinuxImage = "sheyenrath/wiremock.net-alpine";
|
||||
private const string DefaultLinuxMappingsPath = "/app/__admin/mappings";
|
||||
|
||||
/// <summary>
|
||||
/// Adds a WireMock.Net Server resource to the application model.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
|
||||
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency.</param>
|
||||
/// <param name="port">The HTTP port for the WireMock Server.</param>
|
||||
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
|
||||
public static IResourceBuilder<WireMockServerResource> AddWireMock(this IDistributedApplicationBuilder builder, string name, int? port = null)
|
||||
{
|
||||
Guard.NotNull(builder);
|
||||
Guard.NotNullOrWhiteSpace(name);
|
||||
Guard.Condition(port, p => p is null or > 0 and <= ushort.MaxValue);
|
||||
|
||||
return builder.AddWireMock(name, callback =>
|
||||
{
|
||||
callback.HttpPort = port;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a WireMock.Net Server resource to the application model.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
|
||||
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency.</param>
|
||||
/// <param name="arguments">The arguments to start the WireMock.Net Server.</param>
|
||||
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
|
||||
public static IResourceBuilder<WireMockServerResource> AddWireMock(this IDistributedApplicationBuilder builder, string name, WireMockServerArguments arguments)
|
||||
{
|
||||
Guard.NotNull(builder);
|
||||
Guard.NotNullOrWhiteSpace(name);
|
||||
Guard.NotNull(arguments);
|
||||
|
||||
var wireMockContainerResource = new WireMockServerResource(name, arguments);
|
||||
var resourceBuilder = builder
|
||||
.AddResource(wireMockContainerResource)
|
||||
.WithImage(DefaultLinuxImage)
|
||||
.WithEnvironment(ctx => ctx.EnvironmentVariables.Add("DOTNET_USE_POLLING_FILE_WATCHER", "1")) // https://khalidabuhakmeh.com/aspnet-docker-gotchas-and-workarounds#configuration-reloads-and-filesystemwatcher
|
||||
.WithHttpEndpoint(port: arguments.HttpPort, targetPort: WireMockServerArguments.HttpContainerPort)
|
||||
.WithWireMockInspectorCommand();
|
||||
|
||||
if (!string.IsNullOrEmpty(arguments.MappingsPath))
|
||||
{
|
||||
resourceBuilder = resourceBuilder.WithBindMount(arguments.MappingsPath, DefaultLinuxMappingsPath);
|
||||
}
|
||||
|
||||
resourceBuilder = resourceBuilder.WithArgs(ctx =>
|
||||
{
|
||||
foreach (var arg in arguments.GetArgs())
|
||||
{
|
||||
ctx.Args.Add(arg);
|
||||
}
|
||||
});
|
||||
|
||||
return resourceBuilder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a WireMock.Net Server resource to the application model.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
|
||||
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency.</param>
|
||||
/// <param name="callback">A callback that allows for setting the <see cref="WireMockServerArguments"/>.</param>
|
||||
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
|
||||
public static IResourceBuilder<WireMockServerResource> AddWireMock(this IDistributedApplicationBuilder builder, string name, Action<WireMockServerArguments> callback)
|
||||
{
|
||||
Guard.NotNull(builder);
|
||||
Guard.NotNullOrWhiteSpace(name);
|
||||
Guard.NotNull(callback);
|
||||
|
||||
var arguments = new WireMockServerArguments();
|
||||
callback(arguments);
|
||||
|
||||
return builder.AddWireMock(name, arguments);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines if the static mappings should be read at startup.
|
||||
///
|
||||
/// Default set to <c>false</c>.
|
||||
/// </summary>
|
||||
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
|
||||
public static IResourceBuilder<WireMockServerResource> WithReadStaticMappings(this IResourceBuilder<WireMockServerResource> wiremock)
|
||||
{
|
||||
Guard.NotNull(wiremock).Resource.Arguments.ReadStaticMappings = true;
|
||||
return wiremock;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Watch the static mapping files + folder for changes when running.
|
||||
///
|
||||
/// Default set to <c>false</c>.
|
||||
/// </summary>
|
||||
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
|
||||
public static IResourceBuilder<WireMockServerResource> WithWatchStaticMappings(this IResourceBuilder<WireMockServerResource> wiremock)
|
||||
{
|
||||
Guard.NotNull(wiremock).Resource.Arguments.WatchStaticMappings = true;
|
||||
return wiremock;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the path for the (static) mapping json files.
|
||||
/// </summary>
|
||||
/// <param name="wiremock">The <see cref="IResourceBuilder{WireMockServerResource}"/>.</param>
|
||||
/// <param name="mappingsPath">The local path.</param>
|
||||
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
|
||||
public static IResourceBuilder<WireMockServerResource> WithMappingsPath(this IResourceBuilder<WireMockServerResource> wiremock, string mappingsPath)
|
||||
{
|
||||
Guard.NotNullOrWhiteSpace(mappingsPath);
|
||||
Guard.NotNull(wiremock).Resource.Arguments.MappingsPath = mappingsPath;
|
||||
|
||||
return wiremock.WithBindMount(mappingsPath, DefaultLinuxMappingsPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the admin username and password for accessing the admin interface from WireMock.Net via HTTP.
|
||||
/// </summary>
|
||||
/// <param name="wiremock">The <see cref="IResourceBuilder{WireMockServerResource}"/>.</param>
|
||||
/// <param name="username">The admin username.</param>
|
||||
/// <param name="password">The admin password.</param>
|
||||
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
|
||||
public static IResourceBuilder<WireMockServerResource> WithAdminUserNameAndPassword(this IResourceBuilder<WireMockServerResource> wiremock, string username, string password)
|
||||
{
|
||||
Guard.NotNull(wiremock);
|
||||
|
||||
wiremock.Resource.Arguments.AdminUsername = Guard.NotNull(username);
|
||||
wiremock.Resource.Arguments.AdminPassword = Guard.NotNull(password);
|
||||
return wiremock;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use WireMock Client's AdminApiMappingBuilder to configure the WireMock.Net resource.
|
||||
/// </summary>
|
||||
/// <param name="wiremock">The <see cref="IResourceBuilder{WireMockServerResource}"/>.</param>
|
||||
/// <param name="configure">Delegate that will be invoked to configure the WireMock.Net resource.</param>
|
||||
/// <returns></returns>
|
||||
public static IResourceBuilder<WireMockServerResource> WithApiMappingBuilder(this IResourceBuilder<WireMockServerResource> wiremock, Func<AdminApiMappingBuilder, Task> configure)
|
||||
{
|
||||
return wiremock.WithApiMappingBuilder((adminApiMappingBuilder, _) => configure.Invoke(adminApiMappingBuilder));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use WireMock Client's AdminApiMappingBuilder to configure the WireMock.Net resource.
|
||||
/// </summary>
|
||||
/// <param name="wiremock">The <see cref="IResourceBuilder{WireMockServerResource}"/>.</param>
|
||||
/// <param name="configure">Delegate that will be invoked to configure the WireMock.Net resource.</param>
|
||||
/// <returns></returns>
|
||||
public static IResourceBuilder<WireMockServerResource> WithApiMappingBuilder(this IResourceBuilder<WireMockServerResource> wiremock, Func<AdminApiMappingBuilder, CancellationToken, Task> configure)
|
||||
{
|
||||
Guard.NotNull(wiremock);
|
||||
|
||||
wiremock.ApplicationBuilder.Services.TryAddLifecycleHook<WireMockServerLifecycleHook>();
|
||||
wiremock.Resource.Arguments.ApiMappingBuilder = configure;
|
||||
|
||||
return wiremock;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables the WireMockInspect, a cross-platform UI app that facilitates WireMock troubleshooting.
|
||||
/// This requires installation of the WireMockInspector tool.
|
||||
/// <code>
|
||||
/// dotnet tool install WireMockInspector --global --no-cache --ignore-failed-sources
|
||||
/// </code>
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IResourceBuilder{WireMockNetResource}"/>.</param>
|
||||
/// <returns></returns>
|
||||
public static IResourceBuilder<WireMockServerResource> WithWireMockInspectorCommand(this IResourceBuilder<WireMockServerResource> builder)
|
||||
{
|
||||
Guard.NotNull(builder);
|
||||
|
||||
CommandOptions commandOptions = new()
|
||||
{
|
||||
Description = "Requires installation of the WireMockInspector (https://github.com/WireMock-Net/WireMockInspector) tool:\ndotnet tool install WireMockInspector --global --no-cache --ignore-failed-sources",
|
||||
UpdateState = OnUpdateResourceState,
|
||||
IconName = "BoxSearch",
|
||||
IconVariant = IconVariant.Filled
|
||||
};
|
||||
|
||||
builder.WithCommand(
|
||||
name: "wiremock-inspector",
|
||||
displayName: "WireMock Inspector",
|
||||
executeCommand: context => OnRunOpenInspectorCommandAsync(builder),
|
||||
commandOptions: commandOptions);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static Task<ExecuteCommandResult> OnRunOpenInspectorCommandAsync(IResourceBuilder<WireMockServerResource> builder)
|
||||
{
|
||||
WireMockInspector.Inspect(builder.Resource.GetEndpoint().Url);
|
||||
|
||||
return Task.FromResult(CommandResults.Success());
|
||||
}
|
||||
|
||||
private static ResourceCommandState OnUpdateResourceState(UpdateCommandStateContext context)
|
||||
{
|
||||
return context.ResourceSnapshot.HealthStatus is HealthStatus.Healthy
|
||||
? ResourceCommandState.Enabled
|
||||
: ResourceCommandState.Disabled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using Stef.Validation;
|
||||
using WireMock.Server;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace WireMock.FluentAssertions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides assertion methods to verify the number of calls made to a WireMock server.
|
||||
/// This class is used in the context of FluentAssertions.
|
||||
/// </summary>
|
||||
public class WireMockANumberOfCallsAssertions
|
||||
{
|
||||
private readonly IWireMockServer _server;
|
||||
private readonly int _callsCount;
|
||||
private readonly AssertionChain _chain;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WireMockANumberOfCallsAssertions"/> class.
|
||||
/// </summary>
|
||||
/// <param name="server">The WireMock server to assert against.</param>
|
||||
/// <param name="callsCount">The expected number of calls to assert.</param>
|
||||
/// <param name="chain">The assertion chain</param>
|
||||
public WireMockANumberOfCallsAssertions(IWireMockServer server, int callsCount, AssertionChain chain)
|
||||
{
|
||||
_server = Guard.NotNull(server);
|
||||
_callsCount = callsCount;
|
||||
_chain = chain;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an instance of <see cref="WireMockAssertions"/> which can be used to assert the expected number of calls.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="WireMockAssertions"/> instance for asserting the number of calls to the server.</returns>
|
||||
public WireMockAssertions Calls()
|
||||
{
|
||||
return new WireMockAssertions(_server, _callsCount, _chain);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using WireMock.Extensions;
|
||||
using WireMock.Matchers;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace WireMock.FluentAssertions;
|
||||
|
||||
#pragma warning disable CS1591
|
||||
public partial class WireMockAssertions
|
||||
{
|
||||
[CustomAssertion]
|
||||
public AndWhichConstraint<WireMockAssertions, string> AtAbsoluteUrl(string absoluteUrl, string because = "", params object[] becauseArgs)
|
||||
{
|
||||
_ = AtAbsoluteUrl(new ExactMatcher(true, absoluteUrl), because, becauseArgs);
|
||||
|
||||
return new AndWhichConstraint<WireMockAssertions, string>(this, absoluteUrl);
|
||||
}
|
||||
|
||||
[CustomAssertion]
|
||||
public AndWhichConstraint<WireMockAssertions, IStringMatcher> AtAbsoluteUrl(IStringMatcher absoluteUrlMatcher, string because = "", params object[] becauseArgs)
|
||||
{
|
||||
var (filter, condition) = BuildFilterAndCondition(request => absoluteUrlMatcher.IsPerfectMatch(request.AbsoluteUrl));
|
||||
|
||||
var absoluteUrl = absoluteUrlMatcher.GetPatterns().FirstOrDefault().GetPattern();
|
||||
|
||||
_chain
|
||||
.BecauseOf(because, becauseArgs)
|
||||
.Given(() => RequestMessages)
|
||||
.ForCondition(requests => CallsCount == 0 || requests.Any())
|
||||
.FailWith(
|
||||
"Expected {context:wiremockserver} to have been called at address matching the absolute url {0}{reason}, but no calls were made.",
|
||||
absoluteUrl
|
||||
)
|
||||
.Then
|
||||
.ForCondition(condition)
|
||||
.FailWith(
|
||||
"Expected {context:wiremockserver} to have been called at address matching the absolute url {0}{reason}, but didn't find it among the calls to {1}.",
|
||||
_ => absoluteUrl,
|
||||
requests => requests.Select(request => request.AbsoluteUrl)
|
||||
);
|
||||
|
||||
FilterRequestMessages(filter);
|
||||
|
||||
return new AndWhichConstraint<WireMockAssertions, IStringMatcher>(this, absoluteUrlMatcher);
|
||||
}
|
||||
|
||||
[CustomAssertion]
|
||||
public AndWhichConstraint<WireMockAssertions, string> AtUrl(string url, string because = "", params object[] becauseArgs)
|
||||
{
|
||||
_ = AtUrl(new ExactMatcher(true, url), because, becauseArgs);
|
||||
|
||||
return new AndWhichConstraint<WireMockAssertions, string>(this, url);
|
||||
}
|
||||
|
||||
[CustomAssertion]
|
||||
public AndWhichConstraint<WireMockAssertions, IStringMatcher> AtUrl(IStringMatcher urlMatcher, string because = "", params object[] becauseArgs)
|
||||
{
|
||||
var (filter, condition) = BuildFilterAndCondition(request => urlMatcher.IsPerfectMatch(request.Url));
|
||||
|
||||
var url = urlMatcher.GetPatterns().FirstOrDefault().GetPattern();
|
||||
|
||||
_chain
|
||||
.BecauseOf(because, becauseArgs)
|
||||
.Given(() => RequestMessages)
|
||||
.ForCondition(requests => CallsCount == 0 || requests.Any())
|
||||
.FailWith(
|
||||
"Expected {context:wiremockserver} to have been called at address matching the url {0}{reason}, but no calls were made.",
|
||||
url
|
||||
)
|
||||
.Then
|
||||
.ForCondition(condition)
|
||||
.FailWith(
|
||||
"Expected {context:wiremockserver} to have been called at address matching the url {0}{reason}, but didn't find it among the calls to {1}.",
|
||||
_ => url,
|
||||
requests => requests.Select(request => request.Url)
|
||||
);
|
||||
|
||||
FilterRequestMessages(filter);
|
||||
|
||||
return new AndWhichConstraint<WireMockAssertions, IStringMatcher>(this, urlMatcher);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
#pragma warning disable CS1591
|
||||
using System;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace WireMock.FluentAssertions;
|
||||
|
||||
public partial class WireMockAssertions
|
||||
{
|
||||
[CustomAssertion]
|
||||
public AndWhichConstraint<WireMockAssertions, string> FromClientIP(string clientIP, string because = "", params object[] becauseArgs)
|
||||
{
|
||||
var (filter, condition) = BuildFilterAndCondition(request => string.Equals(request.ClientIP, clientIP, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
_chain
|
||||
.BecauseOf(because, becauseArgs)
|
||||
.Given(() => RequestMessages)
|
||||
.ForCondition(requests => CallsCount == 0 || requests.Any())
|
||||
.FailWith(
|
||||
"Expected {context:wiremockserver} to have been called from client IP {0}{reason}, but no calls were made.",
|
||||
clientIP
|
||||
)
|
||||
.Then
|
||||
.ForCondition(condition)
|
||||
.FailWith(
|
||||
"Expected {context:wiremockserver} to have been called from client IP {0}{reason}, but didn't find it among the calls from IP(s) {1}.",
|
||||
_ => clientIP, requests => requests.Select(request => request.ClientIP)
|
||||
);
|
||||
|
||||
FilterRequestMessages(filter);
|
||||
|
||||
return new AndWhichConstraint<WireMockAssertions, string>(this, clientIP);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
#pragma warning disable CS1591
|
||||
using System;
|
||||
using WireMock.Constants;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace WireMock.FluentAssertions;
|
||||
|
||||
public partial class WireMockAssertions
|
||||
{
|
||||
[CustomAssertion]
|
||||
public AndConstraint<WireMockAssertions> UsingConnect(string because = "", params object[] becauseArgs)
|
||||
=> UsingMethod(HttpRequestMethod.CONNECT, because, becauseArgs);
|
||||
|
||||
[CustomAssertion]
|
||||
public AndConstraint<WireMockAssertions> UsingDelete(string because = "", params object[] becauseArgs)
|
||||
=> UsingMethod(HttpRequestMethod.DELETE, because, becauseArgs);
|
||||
|
||||
[CustomAssertion]
|
||||
public AndConstraint<WireMockAssertions> UsingGet(string because = "", params object[] becauseArgs)
|
||||
=> UsingMethod(HttpRequestMethod.GET, because, becauseArgs);
|
||||
|
||||
[CustomAssertion]
|
||||
public AndConstraint<WireMockAssertions> UsingHead(string because = "", params object[] becauseArgs)
|
||||
=> UsingMethod(HttpRequestMethod.HEAD, because, becauseArgs);
|
||||
|
||||
[CustomAssertion]
|
||||
public AndConstraint<WireMockAssertions> UsingOptions(string because = "", params object[] becauseArgs)
|
||||
=> UsingMethod(HttpRequestMethod.OPTIONS, because, becauseArgs);
|
||||
|
||||
[CustomAssertion]
|
||||
public AndConstraint<WireMockAssertions> UsingPost(string because = "", params object[] becauseArgs)
|
||||
=> UsingMethod(HttpRequestMethod.POST, because, becauseArgs);
|
||||
|
||||
[CustomAssertion]
|
||||
public AndConstraint<WireMockAssertions> UsingPatch(string because = "", params object[] becauseArgs)
|
||||
=> UsingMethod(HttpRequestMethod.PATCH, because, becauseArgs);
|
||||
|
||||
[CustomAssertion]
|
||||
public AndConstraint<WireMockAssertions> UsingPut(string because = "", params object[] becauseArgs)
|
||||
=> UsingMethod(HttpRequestMethod.PUT, because, becauseArgs);
|
||||
|
||||
[CustomAssertion]
|
||||
public AndConstraint<WireMockAssertions> UsingTrace(string because = "", params object[] becauseArgs)
|
||||
=> UsingMethod(HttpRequestMethod.TRACE, because, becauseArgs);
|
||||
|
||||
[CustomAssertion]
|
||||
public AndConstraint<WireMockAssertions> UsingAnyMethod(string because = "", params object[] becauseArgs)
|
||||
=> UsingMethod(Any, because, becauseArgs);
|
||||
|
||||
[CustomAssertion]
|
||||
public AndConstraint<WireMockAssertions> UsingMethod(string method, string because = "", params object[] becauseArgs)
|
||||
{
|
||||
var any = method == Any;
|
||||
Func<IRequestMessage, bool> predicate = request => (any && !string.IsNullOrEmpty(request.Method)) ||
|
||||
string.Equals(request.Method, method, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var (filter, condition) = BuildFilterAndCondition(predicate);
|
||||
|
||||
_chain
|
||||
.BecauseOf(because, becauseArgs)
|
||||
.Given(() => RequestMessages)
|
||||
.ForCondition(requests => CallsCount == 0 || requests.Any())
|
||||
.FailWith(
|
||||
"Expected {context:wiremockserver} to have been called using method {0}{reason}, but no calls were made.",
|
||||
method
|
||||
)
|
||||
.Then
|
||||
.ForCondition(condition)
|
||||
.FailWith(
|
||||
"Expected {context:wiremockserver} to have been called using method {0}{reason}, but didn't find it among the methods {1}.",
|
||||
_ => method,
|
||||
requests => requests.Select(request => request.Method)
|
||||
);
|
||||
|
||||
FilterRequestMessages(filter);
|
||||
|
||||
return new AndConstraint<WireMockAssertions>(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
#pragma warning disable CS1591
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AnyOfTypes;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using WireMock.Extensions;
|
||||
using WireMock.Matchers;
|
||||
using WireMock.Models;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace WireMock.FluentAssertions;
|
||||
|
||||
public partial class WireMockAssertions
|
||||
{
|
||||
private const string MessageFormatNoCalls = "Expected {context:wiremockserver} to have been called using body {0}{reason}, but no calls were made.";
|
||||
private const string MessageFormat = "Expected {context:wiremockserver} to have been called using body {0}{reason}, but didn't find it among the body/bodies {1}.";
|
||||
|
||||
[CustomAssertion]
|
||||
public AndConstraint<WireMockAssertions> WithBody(string body, string because = "", params object[] becauseArgs)
|
||||
{
|
||||
return WithBody(new WildcardMatcher(body), because, becauseArgs);
|
||||
}
|
||||
|
||||
[CustomAssertion]
|
||||
public AndConstraint<WireMockAssertions> WithBody(IStringMatcher matcher, string because = "", params object[] becauseArgs)
|
||||
{
|
||||
var (filter, condition) = BuildFilterAndCondition(r => r.Body, matcher);
|
||||
|
||||
return ExecuteAssertionWithBodyStringMatcher(matcher, because, becauseArgs, condition, filter, r => r.Body);
|
||||
}
|
||||
|
||||
[CustomAssertion]
|
||||
public AndConstraint<WireMockAssertions> WithBodyAsJson(object body, string because = "", params object[] becauseArgs)
|
||||
{
|
||||
return WithBodyAsJson(new JsonMatcher(body), because, becauseArgs);
|
||||
}
|
||||
|
||||
[CustomAssertion]
|
||||
public AndConstraint<WireMockAssertions> WithBodyAsJson(string body, string because = "", params object[] becauseArgs)
|
||||
{
|
||||
return WithBodyAsJson(new JsonMatcher(body), because, becauseArgs);
|
||||
}
|
||||
|
||||
[CustomAssertion]
|
||||
public AndConstraint<WireMockAssertions> WithBodyAsJson(IObjectMatcher matcher, string because = "", params object[] becauseArgs)
|
||||
{
|
||||
var (filter, condition) = BuildFilterAndCondition(r => r.BodyAsJson, matcher);
|
||||
|
||||
return ExecuteAssertionWithBodyAsIObjectMatcher(matcher, because, becauseArgs, condition, filter, r => r.BodyAsJson);
|
||||
}
|
||||
|
||||
[CustomAssertion]
|
||||
public AndConstraint<WireMockAssertions> WithBodyAsBytes(byte[] body, string because = "", params object[] becauseArgs)
|
||||
{
|
||||
return WithBodyAsBytes(new ExactObjectMatcher(body), because, becauseArgs);
|
||||
}
|
||||
|
||||
[CustomAssertion]
|
||||
public AndConstraint<WireMockAssertions> WithBodyAsBytes(ExactObjectMatcher matcher, string because = "", params object[] becauseArgs)
|
||||
{
|
||||
var (filter, condition) = BuildFilterAndCondition(r => r.BodyAsBytes, matcher);
|
||||
|
||||
return ExecuteAssertionWithBodyAsIObjectMatcher(matcher, because, becauseArgs, condition, filter, r => r.BodyAsBytes);
|
||||
}
|
||||
|
||||
private AndConstraint<WireMockAssertions> ExecuteAssertionWithBodyStringMatcher(
|
||||
IStringMatcher matcher,
|
||||
string because,
|
||||
object[] becauseArgs,
|
||||
Func<IReadOnlyList<IRequestMessage>, bool> condition,
|
||||
Func<IReadOnlyList<IRequestMessage>, IReadOnlyList<IRequestMessage>> filter,
|
||||
Func<IRequestMessage, object?> expression
|
||||
)
|
||||
{
|
||||
_chain
|
||||
.BecauseOf(because, becauseArgs)
|
||||
.Given(() => RequestMessages)
|
||||
.ForCondition(requests => CallsCount == 0 || requests.Any())
|
||||
.FailWith(
|
||||
MessageFormatNoCalls,
|
||||
FormatBody(matcher.GetPatterns())
|
||||
)
|
||||
.Then
|
||||
.ForCondition(condition)
|
||||
.FailWith(
|
||||
MessageFormat,
|
||||
_ => FormatBody(matcher.GetPatterns()),
|
||||
requests => FormatBodies(requests.Select(expression))
|
||||
);
|
||||
|
||||
FilterRequestMessages(filter);
|
||||
|
||||
return new AndConstraint<WireMockAssertions>(this);
|
||||
}
|
||||
|
||||
private AndConstraint<WireMockAssertions> ExecuteAssertionWithBodyAsIObjectMatcher(
|
||||
IObjectMatcher matcher,
|
||||
string because,
|
||||
object[] becauseArgs,
|
||||
Func<IReadOnlyList<IRequestMessage>, bool> condition,
|
||||
Func<IReadOnlyList<IRequestMessage>, IReadOnlyList<IRequestMessage>> filter,
|
||||
Func<IRequestMessage, object?> expression
|
||||
)
|
||||
{
|
||||
_chain
|
||||
.BecauseOf(because, becauseArgs)
|
||||
.Given(() => RequestMessages)
|
||||
.ForCondition(requests => CallsCount == 0 || requests.Any())
|
||||
.FailWith(
|
||||
MessageFormatNoCalls,
|
||||
FormatBody(matcher.Value)
|
||||
)
|
||||
.Then
|
||||
.ForCondition(condition)
|
||||
.FailWith(
|
||||
MessageFormat,
|
||||
_ => FormatBody(matcher.Value),
|
||||
requests => FormatBodies(requests.Select(expression))
|
||||
);
|
||||
|
||||
FilterRequestMessages(filter);
|
||||
|
||||
return new AndConstraint<WireMockAssertions>(this);
|
||||
}
|
||||
|
||||
private static string? FormatBody(object? body)
|
||||
{
|
||||
return body switch
|
||||
{
|
||||
null => null,
|
||||
string str => str,
|
||||
AnyOf<string, StringPattern>[] stringPatterns => FormatBodies(stringPatterns.Select(p => p.GetPattern())),
|
||||
byte[] bytes => $"byte[{bytes.Length}] {{...}}",
|
||||
JToken jToken => jToken.ToString(Formatting.None),
|
||||
_ => JToken.FromObject(body).ToString(Formatting.None)
|
||||
};
|
||||
}
|
||||
|
||||
private static string? FormatBodies(IEnumerable<object?> bodies)
|
||||
{
|
||||
var valueAsArray = bodies as object[] ?? bodies.ToArray();
|
||||
return valueAsArray.Length == 1 ? FormatBody(valueAsArray[0]) : $"[ {string.Join(", ", valueAsArray.Select(FormatBody))} ]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace WireMock.FluentAssertions;
|
||||
|
||||
public partial class WireMockAssertions
|
||||
{
|
||||
[CustomAssertion]
|
||||
public AndWhichConstraint<WireMockAssertions, string> WitHeaderKey(string expectedKey, string because = "", params object[] becauseArgs)
|
||||
{
|
||||
var (filter, condition) = BuildFilterAndCondition(request =>
|
||||
{
|
||||
return request.Headers?.Any(h => h.Key == expectedKey) == true;
|
||||
});
|
||||
|
||||
_chain
|
||||
.BecauseOf(because, becauseArgs)
|
||||
.Given(() => RequestMessages)
|
||||
.ForCondition(requests => CallsCount == 0 || requests.Any())
|
||||
.FailWith(
|
||||
"Expected {context:wiremockserver} to have been called with Header {0}{reason}.",
|
||||
expectedKey
|
||||
)
|
||||
.Then
|
||||
.ForCondition(condition)
|
||||
.FailWith(
|
||||
"Expected {context:wiremockserver} to have been called with Header {0}{reason}, but didn't find it among the calls with Header(s) {1}.",
|
||||
_ => expectedKey,
|
||||
requests => requests.Select(request => request.Headers)
|
||||
);
|
||||
|
||||
FilterRequestMessages(filter);
|
||||
|
||||
return new AndWhichConstraint<WireMockAssertions, string>(this, expectedKey);
|
||||
}
|
||||
|
||||
[CustomAssertion]
|
||||
public AndConstraint<WireMockAssertions> WithHeader(string expectedKey, string value, string because = "", params object[] becauseArgs)
|
||||
=> WithHeader(expectedKey, new[] { value }, because, becauseArgs);
|
||||
|
||||
[CustomAssertion]
|
||||
public AndConstraint<WireMockAssertions> WithHeader(string expectedKey, string[] expectedValues, string because = "", params object[] becauseArgs)
|
||||
{
|
||||
var (filter, condition) = BuildFilterAndCondition(request =>
|
||||
{
|
||||
var headers = request.Headers?.ToArray() ?? [];
|
||||
|
||||
var matchingHeaderValues = headers.Where(h => h.Key == expectedKey).SelectMany(h => h.Value.ToArray()).ToArray();
|
||||
|
||||
if (expectedValues.Length == 1 && matchingHeaderValues.Length == 1)
|
||||
{
|
||||
return matchingHeaderValues[0] == expectedValues[0];
|
||||
}
|
||||
|
||||
var trimmedHeaderValues = string.Join(",", matchingHeaderValues.Select(x => x)).Split(',').Select(x => x.Trim()).ToArray();
|
||||
return expectedValues.Any(trimmedHeaderValues.Contains);
|
||||
});
|
||||
|
||||
_chain
|
||||
.BecauseOf(because, becauseArgs)
|
||||
.Given(() => RequestMessages)
|
||||
.ForCondition(requests => CallsCount == 0 || requests.Any())
|
||||
.FailWith(
|
||||
"Expected {context:wiremockserver} to have been called with Header {0} and Values {1}{reason}.",
|
||||
expectedKey,
|
||||
expectedValues
|
||||
)
|
||||
.Then
|
||||
.ForCondition(condition)
|
||||
.FailWith(
|
||||
"Expected {context:wiremockserver} to have been called with Header {0} and Values {1}{reason}, but didn't find it among the calls with Header(s) {2}.",
|
||||
_ => expectedKey,
|
||||
_ => expectedValues,
|
||||
requests => requests.Select(request => request.Headers)
|
||||
);
|
||||
|
||||
FilterRequestMessages(filter);
|
||||
|
||||
return new AndConstraint<WireMockAssertions>(this);
|
||||
}
|
||||
|
||||
[CustomAssertion]
|
||||
public AndConstraint<WireMockAssertions> WithoutHeaderKey(string unexpectedKey, string because = "", params object[] becauseArgs)
|
||||
{
|
||||
var (filter, condition) = BuildFilterAndCondition(request =>
|
||||
{
|
||||
return request.Headers?.Any(h => h.Key == unexpectedKey) != true;
|
||||
});
|
||||
|
||||
_chain
|
||||
.BecauseOf(because, becauseArgs)
|
||||
.Given(() => RequestMessages)
|
||||
.ForCondition(requests => CallsCount == 0 || requests.Any())
|
||||
.FailWith(
|
||||
"Expected {context:wiremockserver} not to have been called with Header {0}{reason}.",
|
||||
unexpectedKey
|
||||
)
|
||||
.Then
|
||||
.ForCondition(condition)
|
||||
.FailWith(
|
||||
"Expected {context:wiremockserver} not to have been called with Header {0}{reason}, but found it among the calls with Header(s) {1}.",
|
||||
_ => unexpectedKey,
|
||||
requests => requests.Select(request => request.Headers)
|
||||
);
|
||||
|
||||
FilterRequestMessages(filter);
|
||||
|
||||
return new AndConstraint<WireMockAssertions>(this);
|
||||
}
|
||||
|
||||
[CustomAssertion]
|
||||
public AndConstraint<WireMockAssertions> WithoutHeader(string unexpectedKey, string value, string because = "", params object[] becauseArgs)
|
||||
=> WithoutHeader(unexpectedKey, new[] { value }, because, becauseArgs);
|
||||
|
||||
[CustomAssertion]
|
||||
public AndConstraint<WireMockAssertions> WithoutHeader(string unexpectedKey, string[] expectedValues, string because = "", params object[] becauseArgs)
|
||||
{
|
||||
var (filter, condition) = BuildFilterAndCondition(request =>
|
||||
{
|
||||
var headers = request.Headers?.ToArray() ?? [];
|
||||
|
||||
var matchingHeaderValues = headers.Where(h => h.Key == unexpectedKey).SelectMany(h => h.Value.ToArray()).ToArray();
|
||||
|
||||
if (expectedValues.Length == 1 && matchingHeaderValues.Length == 1)
|
||||
{
|
||||
return matchingHeaderValues[0] != expectedValues[0];
|
||||
}
|
||||
|
||||
var trimmedHeaderValues = string.Join(",", matchingHeaderValues.Select(x => x)).Split(',').Select(x => x.Trim()).ToArray();
|
||||
return !expectedValues.Any(trimmedHeaderValues.Contains);
|
||||
});
|
||||
|
||||
_chain
|
||||
.BecauseOf(because, becauseArgs)
|
||||
.Given(() => RequestMessages)
|
||||
.ForCondition(requests => CallsCount == 0 || requests.Any())
|
||||
.FailWith(
|
||||
"Expected {context:wiremockserver} not to have been called with Header {0} and Values {1}{reason}.",
|
||||
unexpectedKey,
|
||||
expectedValues
|
||||
)
|
||||
.Then
|
||||
.ForCondition(condition)
|
||||
.FailWith(
|
||||
"Expected {context:wiremockserver} not to have been called with Header {0} and Values {1}{reason}, but found it among the calls with Header(s) {2}.",
|
||||
_ => unexpectedKey,
|
||||
_ => expectedValues,
|
||||
requests => requests.Select(request => request.Headers)
|
||||
);
|
||||
|
||||
FilterRequestMessages(filter);
|
||||
|
||||
return new AndConstraint<WireMockAssertions>(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
#pragma warning disable CS1591
|
||||
using System;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace WireMock.FluentAssertions;
|
||||
|
||||
public partial class WireMockAssertions
|
||||
{
|
||||
[CustomAssertion]
|
||||
public AndWhichConstraint<WireMockAssertions, string> WithProxyUrl(string proxyUrl, string because = "", params object[] becauseArgs)
|
||||
{
|
||||
var (filter, condition) = BuildFilterAndCondition(request => string.Equals(request.ProxyUrl, proxyUrl, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
_chain
|
||||
.BecauseOf(because, becauseArgs)
|
||||
.Given(() => RequestMessages)
|
||||
.ForCondition(requests => CallsCount == 0 || requests.Any())
|
||||
.FailWith(
|
||||
"Expected {context:wiremockserver} to have been called with proxy url {0}{reason}, but no calls were made.",
|
||||
proxyUrl
|
||||
)
|
||||
.Then
|
||||
.ForCondition(condition)
|
||||
.FailWith(
|
||||
"Expected {context:wiremockserver} to have been called with proxy url {0}{reason}, but didn't find it among the calls with {1}.",
|
||||
_ => proxyUrl,
|
||||
requests => requests.Select(request => request.ProxyUrl)
|
||||
);
|
||||
|
||||
FilterRequestMessages(filter);
|
||||
|
||||
return new AndWhichConstraint<WireMockAssertions, string>(this, proxyUrl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
#pragma warning disable CS1591
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using WireMock.Matchers;
|
||||
using WireMock.Server;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace WireMock.FluentAssertions;
|
||||
|
||||
public partial class WireMockAssertions
|
||||
{
|
||||
public const string Any = "*";
|
||||
|
||||
public int? CallsCount { get; }
|
||||
public IReadOnlyList<IRequestMessage> RequestMessages { get; private set; }
|
||||
private readonly AssertionChain _chain;
|
||||
|
||||
public WireMockAssertions(IWireMockServer subject, int? callsCount, AssertionChain chain)
|
||||
{
|
||||
CallsCount = callsCount;
|
||||
RequestMessages = subject.LogEntries.Select(logEntry => logEntry.RequestMessage).ToList();
|
||||
_chain = chain;
|
||||
}
|
||||
|
||||
public (Func<IReadOnlyList<IRequestMessage>, IReadOnlyList<IRequestMessage>> Filter, Func<IReadOnlyList<IRequestMessage>, bool> Condition) BuildFilterAndCondition(Func<IRequestMessage, bool> predicate)
|
||||
{
|
||||
Func<IReadOnlyList<IRequestMessage>, IReadOnlyList<IRequestMessage>> filter = requests => requests.Where(predicate).ToList();
|
||||
|
||||
return (filter, requests => (CallsCount is null && filter(requests).Any()) || CallsCount == filter(requests).Count);
|
||||
}
|
||||
|
||||
public (Func<IReadOnlyList<IRequestMessage>, IReadOnlyList<IRequestMessage>> Filter, Func<IReadOnlyList<IRequestMessage>, bool> Condition) BuildFilterAndCondition(Func<IRequestMessage, string?> expression, IStringMatcher matcher)
|
||||
{
|
||||
return BuildFilterAndCondition(r => matcher.IsMatch(expression(r)).IsPerfect());
|
||||
}
|
||||
|
||||
public (Func<IReadOnlyList<IRequestMessage>, IReadOnlyList<IRequestMessage>> Filter, Func<IReadOnlyList<IRequestMessage>, bool> Condition) BuildFilterAndCondition(Func<IRequestMessage, object?> expression, IObjectMatcher matcher)
|
||||
{
|
||||
return BuildFilterAndCondition(r => matcher.IsMatch(expression(r)).IsPerfect());
|
||||
}
|
||||
|
||||
public void FilterRequestMessages(Func<IReadOnlyList<IRequestMessage>, IReadOnlyList<IRequestMessage>> filter)
|
||||
{
|
||||
RequestMessages = filter(RequestMessages).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using FluentAssertions.Primitives;
|
||||
using WireMock.Server;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace WireMock.FluentAssertions;
|
||||
|
||||
/// <summary>
|
||||
/// Contains a number of methods to assert that the <see cref="IWireMockServer"/> is in the expected state.
|
||||
/// </summary>
|
||||
public class WireMockReceivedAssertions : ReferenceTypeAssertions<IWireMockServer, WireMockReceivedAssertions>
|
||||
{
|
||||
private readonly AssertionChain _chain;
|
||||
|
||||
/// <summary>
|
||||
/// Create a WireMockReceivedAssertions.
|
||||
/// </summary>
|
||||
/// <param name="server">The <see cref="IWireMockServer"/>.</param>
|
||||
/// <param name="chain">The assertion chain</param>
|
||||
public WireMockReceivedAssertions(IWireMockServer server, AssertionChain chain) : base(server, chain)
|
||||
{
|
||||
_chain = chain;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asserts if <see cref="IWireMockServer"/> has received no calls.
|
||||
/// </summary>
|
||||
/// <returns><see cref="WireMockAssertions"/></returns>
|
||||
public WireMockAssertions HaveReceivedNoCalls()
|
||||
{
|
||||
return new WireMockAssertions(Subject, 0, _chain);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asserts if <see cref="IWireMockServer"/> has received a call.
|
||||
/// </summary>
|
||||
/// <returns><see cref="WireMockAssertions"/></returns>
|
||||
public WireMockAssertions HaveReceivedACall()
|
||||
{
|
||||
return new WireMockAssertions(Subject, null, _chain);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asserts if <see cref="IWireMockServer"/> has received n-calls.
|
||||
/// </summary>
|
||||
/// <param name="callsCount"></param>
|
||||
/// <returns><see cref="WireMockANumberOfCallsAssertions"/></returns>
|
||||
public WireMockANumberOfCallsAssertions HaveReceived(int callsCount)
|
||||
{
|
||||
return new WireMockANumberOfCallsAssertions(Subject, callsCount, _chain);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string Identifier => "wiremockserver";
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using WireMock.Server;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace WireMock.FluentAssertions
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains extension methods for custom assertions in unit tests.
|
||||
/// </summary>
|
||||
public static class WireMockExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a <see cref="WireMockReceivedAssertions"/> object that can be used to assert the current <see cref="IWireMockServer"/>.
|
||||
/// </summary>
|
||||
/// <param name="instance">The WireMockServer</param>
|
||||
/// <returns><see cref="WireMockReceivedAssertions"/></returns>
|
||||
public static WireMockReceivedAssertions Should(this IWireMockServer instance)
|
||||
{
|
||||
return new WireMockReceivedAssertions(instance, AssertionChain.GetOrCreate());
|
||||
}
|
||||
}
|
||||
}
|
||||
5
src/WireMock.Net.AwesomeAssertions/Usings.cs
Normal file
5
src/WireMock.Net.AwesomeAssertions/Usings.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
global using System.Linq;
|
||||
global using FluentAssertions;
|
||||
global using FluentAssertions.Execution;
|
||||
@@ -0,0 +1,39 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>AwesomeAssertions extensions for WireMock.Net</Description>
|
||||
<AssemblyTitle>WireMock.Net.AwesomeAssertions</AssemblyTitle>
|
||||
<Authors>Francesco Venturoli;Mahmoud Ali;Stef Heyenrath</Authors>
|
||||
<TargetFrameworks>net47;netstandard2.0;netstandard2.1</TargetFrameworks>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<AssemblyName>WireMock.Net.AwesomeAssertions</AssemblyName>
|
||||
<PackageId>WireMock.Net.AwesomeAssertions</PackageId>
|
||||
<PackageTags>wiremock;AwesomeAssertions;UnitTest;Assert;Assertions</PackageTags>
|
||||
<RootNamespace>WireMock.AwesomeAssertions</RootNamespace>
|
||||
<ProjectGuid>{9565C395-FC5D-4CB1-8381-EC3D9DA74779}</ProjectGuid>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
|
||||
<CodeAnalysisRuleSet>../WireMock.Net/WireMock.Net.ruleset</CodeAnalysisRuleSet>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>../WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
|
||||
<!--<DelaySign>true</DelaySign>-->
|
||||
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AwesomeAssertions" Version="8.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\WireMock.Net\WireMock.Net.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,6 +1,6 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
#if NET46 || NETSTANDARD2_0
|
||||
#if NET46 || NET47 || NETSTANDARD2_0
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace WireMock.Net.OpenApiParser.Extensions;
|
||||
|
||||
@@ -1,60 +1,53 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Interfaces;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Microsoft.OpenApi.Models.Interfaces;
|
||||
using WireMock.Net.OpenApiParser.Types;
|
||||
|
||||
namespace WireMock.Net.OpenApiParser.Extensions;
|
||||
|
||||
internal static class OpenApiSchemaExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// https://stackoverflow.com/questions/48111459/how-to-define-a-property-that-can-be-string-or-null-in-openapi-swagger
|
||||
/// </summary>
|
||||
public static bool TryGetXNullable(this OpenApiSchema schema, out bool value)
|
||||
public static bool TryGetXNullable(this IOpenApiSchema schema, out bool value)
|
||||
{
|
||||
value = false;
|
||||
|
||||
if (schema.Extensions.TryGetValue("x-nullable", out var e) && e is OpenApiBoolean openApiBoolean)
|
||||
if (schema.Extensions != null && schema.Extensions.TryGetValue(OpenApiConstants.NullableExtension, out var nullExtRawValue) && nullExtRawValue is OpenApiAny { Node: { } jsonNode })
|
||||
{
|
||||
value = openApiBoolean.Value;
|
||||
value = jsonNode.GetValueKind() == JsonValueKind.True;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static SchemaType GetSchemaType(this OpenApiSchema? schema)
|
||||
public static JsonSchemaType? GetSchemaType(this IOpenApiSchema? schema, out bool isNullable)
|
||||
{
|
||||
isNullable = false;
|
||||
|
||||
if (schema == null)
|
||||
{
|
||||
return SchemaType.Unknown;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (schema.Type == null)
|
||||
{
|
||||
if (schema.AllOf.Any() || schema.AnyOf.Any())
|
||||
if (schema.AllOf?.Any() == true || schema.AnyOf?.Any() == true)
|
||||
{
|
||||
return SchemaType.Object;
|
||||
return JsonSchemaType.Object;
|
||||
}
|
||||
}
|
||||
|
||||
return schema.Type switch
|
||||
{
|
||||
"object" => SchemaType.Object,
|
||||
"array" => SchemaType.Array,
|
||||
"integer" => SchemaType.Integer,
|
||||
"number" => SchemaType.Number,
|
||||
"boolean" => SchemaType.Boolean,
|
||||
"string" => SchemaType.String,
|
||||
"file" => SchemaType.File,
|
||||
_ => SchemaType.Unknown
|
||||
};
|
||||
isNullable = (schema.Type | JsonSchemaType.Null) == JsonSchemaType.Null || (schema.TryGetXNullable(out var xNullable) && xNullable);
|
||||
|
||||
// Removes the Null flag from the schema.Type, ensuring the returned value represents a non-nullable type.
|
||||
return schema.Type & ~JsonSchemaType.Null;
|
||||
}
|
||||
|
||||
public static SchemaFormat GetSchemaFormat(this OpenApiSchema? schema)
|
||||
public static SchemaFormat GetSchemaFormat(this IOpenApiSchema? schema)
|
||||
{
|
||||
switch (schema?.Format)
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Microsoft.OpenApi.Readers;
|
||||
using Microsoft.OpenApi.Reader;
|
||||
using Stef.Validation;
|
||||
using WireMock.Net.OpenApiParser.Settings;
|
||||
using WireMock.Server;
|
||||
@@ -17,7 +17,7 @@ namespace WireMock.Net.OpenApiParser.Extensions;
|
||||
public static class WireMockServerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Register the mappings via an OpenAPI (swagger) V2 or V3 file.
|
||||
/// Register the mappings via an OpenAPI (swagger) V2/V3/V3.1 file.
|
||||
/// </summary>
|
||||
/// <param name="server">The WireMockServer instance</param>
|
||||
/// <param name="path">Path containing OpenAPI file to parse and use the mappings.</param>
|
||||
@@ -29,7 +29,7 @@ public static class WireMockServerExtensions
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register the mappings via an OpenAPI (swagger) V2 or V3 file.
|
||||
/// Register the mappings via an OpenAPI (swagger) V2/V3/V3.1 file.
|
||||
/// </summary>
|
||||
/// <param name="server">The WireMockServer instance</param>
|
||||
/// <param name="path">Path containing OpenAPI file to parse and use the mappings.</param>
|
||||
@@ -47,7 +47,7 @@ public static class WireMockServerExtensions
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register the mappings via an OpenAPI (swagger) V2 or V3 stream.
|
||||
/// Register the mappings via an OpenAPI (swagger) V2/V3/V3.1 stream.
|
||||
/// </summary>
|
||||
/// <param name="server">The WireMockServer instance</param>
|
||||
/// <param name="stream">Stream containing OpenAPI description to parse and use the mappings.</param>
|
||||
@@ -59,7 +59,7 @@ public static class WireMockServerExtensions
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register the mappings via an OpenAPI (swagger) V2 or V3 stream.
|
||||
/// Register the mappings via an OpenAPI (swagger) V2/V3/V3.1 stream.
|
||||
/// </summary>
|
||||
/// <param name="server">The WireMockServer instance</param>
|
||||
/// <param name="stream">Stream containing OpenAPI description to parse and use the mappings.</param>
|
||||
@@ -78,7 +78,7 @@ public static class WireMockServerExtensions
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register the mappings via an OpenAPI (swagger) V2 or V3 document.
|
||||
/// Register the mappings via an OpenAPI (swagger) V2/V3/V3.1 document.
|
||||
/// </summary>
|
||||
/// <param name="server">The WireMockServer instance</param>
|
||||
/// <param name="document">The OpenAPI document to use as mappings.</param>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Microsoft.OpenApi.Readers;
|
||||
using Microsoft.OpenApi.Reader;
|
||||
using WireMock.Admin.Mappings;
|
||||
using WireMock.Net.OpenApiParser.Settings;
|
||||
|
||||
@@ -17,7 +17,7 @@ public interface IWireMockOpenApiParser
|
||||
/// <summary>
|
||||
/// Generate <see cref="IReadOnlyList{MappingModel}"/> from a file-path.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to read the OpenApi/Swagger/V2/V3 or Raml file.</param>
|
||||
/// <param name="path">The path to read the OpenApi/Swagger/V2/V3/V31 or Raml file.</param>
|
||||
/// <param name="diagnostic">OpenApiDiagnostic output</param>
|
||||
/// <returns>MappingModel</returns>
|
||||
IReadOnlyList<MappingModel> FromFile(string path, out OpenApiDiagnostic diagnostic);
|
||||
@@ -25,7 +25,7 @@ public interface IWireMockOpenApiParser
|
||||
/// <summary>
|
||||
/// Generate <see cref="IReadOnlyList{MappingModel}"/> from a file-path.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to read the OpenApi/Swagger/V2/V3 or Raml file.</param>
|
||||
/// <param name="path">The path to read the OpenApi/Swagger/V2/V3/V31 or Raml file.</param>
|
||||
/// <param name="settings">Additional settings</param>
|
||||
/// <param name="diagnostic">OpenApiDiagnostic output</param>
|
||||
/// <returns>MappingModel</returns>
|
||||
|
||||
@@ -3,20 +3,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.OpenApi;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Microsoft.OpenApi.Writers;
|
||||
using Microsoft.OpenApi.Models.Interfaces;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Stef.Validation;
|
||||
using WireMock.Admin.Mappings;
|
||||
using WireMock.Net.OpenApiParser.Extensions;
|
||||
using WireMock.Net.OpenApiParser.Settings;
|
||||
using WireMock.Net.OpenApiParser.Types;
|
||||
using WireMock.Net.OpenApiParser.Utils;
|
||||
using SystemTextJsonSerializer = System.Text.Json.JsonSerializer;
|
||||
|
||||
namespace WireMock.Net.OpenApiParser.Mappers;
|
||||
|
||||
@@ -39,56 +38,40 @@ internal class OpenApiPathsMapper
|
||||
.OrderBy(p => p.Key)
|
||||
.Select(p => MapPath(p.Key, p.Value, servers))
|
||||
.SelectMany(x => x)
|
||||
.ToArray() ??
|
||||
Array.Empty<MappingModel>();
|
||||
.ToArray() ?? [];
|
||||
}
|
||||
|
||||
private IReadOnlyList<MappingModel> MapPaths(OpenApiPaths? paths, IList<OpenApiServer> servers)
|
||||
private IReadOnlyList<MappingModel> MapPath(string path, IOpenApiPathItem pathItem, IList<OpenApiServer> servers)
|
||||
{
|
||||
return paths?
|
||||
.OrderBy(p => p.Key)
|
||||
.Select(p => MapPath(p.Key, p.Value, servers))
|
||||
.SelectMany(x => x)
|
||||
.ToArray() ??
|
||||
Array.Empty<MappingModel>();
|
||||
}
|
||||
|
||||
private IReadOnlyList<MappingModel> MapPath(string path, OpenApiPathItem pathItem, IList<OpenApiServer> servers)
|
||||
{
|
||||
return pathItem.Operations.Select(o => MapOperationToMappingModel(path, o.Key.ToString().ToUpperInvariant(), o.Value, servers)).ToArray();
|
||||
return pathItem.Operations?.Select(o => MapOperationToMappingModel(path, o.Key.ToString().ToUpperInvariant(), o.Value, servers)).ToArray() ?? [];
|
||||
}
|
||||
|
||||
private MappingModel MapOperationToMappingModel(string path, string httpMethod, OpenApiOperation operation, IList<OpenApiServer> servers)
|
||||
{
|
||||
var queryParameters = operation.Parameters.Where(p => p.In == ParameterLocation.Query);
|
||||
var pathParameters = operation.Parameters.Where(p => p.In == ParameterLocation.Path);
|
||||
var headers = operation.Parameters.Where(p => p.In == ParameterLocation.Header);
|
||||
var queryParameters = operation.Parameters?.Where(p => p.In == ParameterLocation.Query) ?? [];
|
||||
var pathParameters = operation.Parameters?.Where(p => p.In == ParameterLocation.Path) ?? [];
|
||||
var headers = operation.Parameters?.Where(p => p.In == ParameterLocation.Header) ?? [];
|
||||
|
||||
var response = operation.Responses.FirstOrDefault();
|
||||
var response = operation?.Responses?.FirstOrDefault() ?? new KeyValuePair<string, IOpenApiResponse>();
|
||||
|
||||
TryGetContent(response.Value?.Content, out OpenApiMediaType? responseContent, out string? responseContentType);
|
||||
TryGetContent(response.Value?.Content, out OpenApiMediaType? responseContent, out var responseContentType);
|
||||
var responseSchema = response.Value?.Content?.FirstOrDefault().Value?.Schema;
|
||||
var responseExample = responseContent?.Example;
|
||||
var responseSchemaExample = responseContent?.Schema?.Example;
|
||||
|
||||
var body = responseExample != null ? MapOpenApiAnyToJToken(responseExample) :
|
||||
responseSchemaExample != null ? MapOpenApiAnyToJToken(responseSchemaExample) :
|
||||
MapSchemaToObject(responseSchema);
|
||||
var responseBody = responseExample ?? responseSchemaExample ?? MapSchemaToObject(responseSchema);
|
||||
|
||||
var requestBodyModel = new BodyModel();
|
||||
if (operation.RequestBody != null && operation.RequestBody.Content != null && operation.RequestBody.Required)
|
||||
{
|
||||
var request = operation.RequestBody.Content;
|
||||
TryGetContent(request, out OpenApiMediaType? requestContent, out _);
|
||||
TryGetContent(request, out var requestContent, out _);
|
||||
|
||||
var requestBodySchema = operation.RequestBody.Content.First().Value?.Schema;
|
||||
var requestBodyExample = requestContent!.Example;
|
||||
var requestBodySchemaExample = requestContent.Schema?.Example;
|
||||
|
||||
var requestBodyMapped = requestBodyExample != null ? MapOpenApiAnyToJToken(requestBodyExample) :
|
||||
requestBodySchemaExample != null ? MapOpenApiAnyToJToken(requestBodySchemaExample) :
|
||||
MapSchemaToObject(requestBodySchema);
|
||||
|
||||
var requestBodyMapped = requestBodyExample ?? requestBodySchemaExample ?? MapSchemaToObject(requestBodySchema);
|
||||
requestBodyModel = MapRequestBody(requestBodyMapped);
|
||||
}
|
||||
|
||||
@@ -102,8 +85,8 @@ internal class OpenApiPathsMapper
|
||||
Guid = Guid.NewGuid(),
|
||||
Request = new RequestModel
|
||||
{
|
||||
Methods = new[] { httpMethod },
|
||||
Path = MapBasePath(servers) + MapPathWithParameters(path, pathParameters),
|
||||
Methods = [httpMethod],
|
||||
Path = PathUtils.Combine(MapBasePath(servers), MapPathWithParameters(path, pathParameters)),
|
||||
Params = MapQueryParameters(queryParameters),
|
||||
Headers = MapRequestHeaders(headers),
|
||||
Body = requestBodyModel
|
||||
@@ -112,12 +95,12 @@ internal class OpenApiPathsMapper
|
||||
{
|
||||
StatusCode = httpStatusCode,
|
||||
Headers = MapHeaders(responseContentType, response.Value?.Headers),
|
||||
BodyAsJson = body
|
||||
BodyAsJson = responseBody != null ? JsonConvert.DeserializeObject(SystemTextJsonSerializer.Serialize(responseBody)) : null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private BodyModel? MapRequestBody(object? requestBody)
|
||||
private BodyModel? MapRequestBody(JsonNode? requestBody)
|
||||
{
|
||||
if (requestBody == null)
|
||||
{
|
||||
@@ -129,7 +112,7 @@ internal class OpenApiPathsMapper
|
||||
Matcher = new MatcherModel
|
||||
{
|
||||
Name = "JsonMatcher",
|
||||
Pattern = JsonConvert.SerializeObject(requestBody, Formatting.Indented),
|
||||
Pattern = SystemTextJsonSerializer.Serialize(requestBody, new JsonSerializerOptions { WriteIndented = true }),
|
||||
IgnoreCase = _settings.RequestBodyIgnoreCase
|
||||
}
|
||||
};
|
||||
@@ -160,117 +143,103 @@ internal class OpenApiPathsMapper
|
||||
return true;
|
||||
}
|
||||
|
||||
private object? MapSchemaToObject(OpenApiSchema? schema, string? name = null)
|
||||
private JsonNode? MapSchemaToObject(IOpenApiSchema? schema)
|
||||
{
|
||||
if (schema == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (schema.GetSchemaType())
|
||||
switch (schema.GetSchemaType(out _))
|
||||
{
|
||||
case SchemaType.Array:
|
||||
var jArray = new JArray();
|
||||
for (int i = 0; i < _settings.NumberOfArrayItems; i++)
|
||||
case JsonSchemaType.Array:
|
||||
var array = new JsonArray();
|
||||
for (var i = 0; i < _settings.NumberOfArrayItems; i++)
|
||||
{
|
||||
if (schema.Items.Properties.Count > 0)
|
||||
if (schema.Items?.Properties?.Count > 0)
|
||||
{
|
||||
var arrayItem = new JObject();
|
||||
var item = new JsonObject();
|
||||
foreach (var property in schema.Items.Properties)
|
||||
{
|
||||
var objectValue = MapSchemaToObject(property.Value, property.Key);
|
||||
if (objectValue is JProperty jp)
|
||||
{
|
||||
arrayItem.Add(jp);
|
||||
}
|
||||
else
|
||||
{
|
||||
arrayItem.Add(new JProperty(property.Key, objectValue));
|
||||
}
|
||||
item[property.Key] = MapSchemaToObject(property.Value);
|
||||
}
|
||||
|
||||
jArray.Add(arrayItem);
|
||||
array.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
var arrayItem = MapSchemaToObject(schema.Items, name: null); // Set name to null to force JObject instead of JProperty
|
||||
jArray.Add(arrayItem);
|
||||
var arrayItem = MapSchemaToObject(schema.Items);
|
||||
array.Add(arrayItem);
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.AllOf.Count > 0)
|
||||
if (schema.AllOf?.Count > 0)
|
||||
{
|
||||
jArray.Add(MapSchemaAllOfToObject(schema));
|
||||
array.Add(MapSchemaAllOfToObject(schema));
|
||||
}
|
||||
|
||||
return jArray;
|
||||
return array;
|
||||
|
||||
case SchemaType.Boolean:
|
||||
case SchemaType.Integer:
|
||||
case SchemaType.Number:
|
||||
case SchemaType.String:
|
||||
case JsonSchemaType.Boolean:
|
||||
case JsonSchemaType.Integer:
|
||||
case JsonSchemaType.Number:
|
||||
case JsonSchemaType.String:
|
||||
return _exampleValueGenerator.GetExampleValue(schema);
|
||||
|
||||
case SchemaType.Object:
|
||||
var propertyAsJObject = new JObject();
|
||||
foreach (var schemaProperty in schema.Properties)
|
||||
case JsonSchemaType.Object:
|
||||
var propertyAsJsonObject = new JsonObject();
|
||||
foreach (var schemaProperty in schema.Properties ?? new Dictionary<string, IOpenApiSchema>())
|
||||
{
|
||||
propertyAsJObject.Add(MapPropertyAsJObject(schemaProperty.Value, schemaProperty.Key));
|
||||
propertyAsJsonObject[schemaProperty.Key] = MapPropertyAsJsonNode(schemaProperty.Value);
|
||||
}
|
||||
|
||||
if (schema.AllOf.Count > 0)
|
||||
if (schema.AllOf?.Count > 0)
|
||||
{
|
||||
foreach (var group in schema.AllOf.SelectMany(p => p.Properties).GroupBy(x => x.Key))
|
||||
foreach (var group in schema.AllOf.SelectMany(p => p.Properties ?? new Dictionary<string, IOpenApiSchema>()).GroupBy(x => x.Key))
|
||||
{
|
||||
propertyAsJObject.Add(MapPropertyAsJObject(group.First().Value, group.Key));
|
||||
propertyAsJsonObject[group.Key] = MapPropertyAsJsonNode(group.First().Value);
|
||||
}
|
||||
}
|
||||
|
||||
return name != null ? new JProperty(name, propertyAsJObject) : propertyAsJObject;
|
||||
return propertyAsJsonObject;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private JObject MapSchemaAllOfToObject(OpenApiSchema schema)
|
||||
private JsonObject MapSchemaAllOfToObject(IOpenApiSchema schema)
|
||||
{
|
||||
var arrayItem = new JObject();
|
||||
foreach (var property in schema.AllOf)
|
||||
var arrayItem = new JsonObject();
|
||||
foreach (var property in schema.AllOf ?? [])
|
||||
{
|
||||
foreach (var item in property.Properties)
|
||||
foreach (var item in property.Properties ?? new Dictionary<string, IOpenApiSchema>())
|
||||
{
|
||||
arrayItem.Add(MapPropertyAsJObject(item.Value, item.Key));
|
||||
arrayItem[item.Key] = MapPropertyAsJsonNode(item.Value);
|
||||
}
|
||||
}
|
||||
return arrayItem;
|
||||
}
|
||||
|
||||
private object MapPropertyAsJObject(OpenApiSchema openApiSchema, string key)
|
||||
private JsonNode? MapPropertyAsJsonNode(IOpenApiSchema openApiSchema)
|
||||
{
|
||||
if (openApiSchema.GetSchemaType() == SchemaType.Object || openApiSchema.GetSchemaType() == SchemaType.Array)
|
||||
var schemaType = openApiSchema.GetSchemaType(out _);
|
||||
if (schemaType is JsonSchemaType.Object or JsonSchemaType.Array)
|
||||
{
|
||||
var mapped = MapSchemaToObject(openApiSchema, key);
|
||||
if (mapped is JProperty jp)
|
||||
{
|
||||
return jp;
|
||||
}
|
||||
|
||||
return new JProperty(key, mapped);
|
||||
return MapSchemaToObject(openApiSchema);
|
||||
}
|
||||
|
||||
// bool propertyIsNullable = openApiSchema.Nullable || (openApiSchema.TryGetXNullable(out bool x) && x);
|
||||
return new JProperty(key, _exampleValueGenerator.GetExampleValue(openApiSchema));
|
||||
return _exampleValueGenerator.GetExampleValue(openApiSchema);
|
||||
}
|
||||
|
||||
private string MapPathWithParameters(string path, IEnumerable<OpenApiParameter>? parameters)
|
||||
private string MapPathWithParameters(string path, IEnumerable<IOpenApiParameter>? parameters)
|
||||
{
|
||||
if (parameters == null)
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
string newPath = path;
|
||||
var newPath = path;
|
||||
foreach (var parameter in parameters)
|
||||
{
|
||||
var exampleMatcherModel = GetExampleMatcherModel(parameter.Schema, _settings.PathPatternToUse);
|
||||
@@ -280,93 +249,56 @@ internal class OpenApiPathsMapper
|
||||
return newPath;
|
||||
}
|
||||
|
||||
private string MapBasePath(IList<OpenApiServer>? servers)
|
||||
private IDictionary<string, object>? MapHeaders(string? responseContentType, IDictionary<string, IOpenApiHeader>? headers)
|
||||
{
|
||||
if (servers == null || servers.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
OpenApiServer server = servers.First();
|
||||
if (Uri.TryCreate(server.Url, UriKind.RelativeOrAbsolute, out Uri uriResult))
|
||||
{
|
||||
return uriResult.IsAbsoluteUri ? uriResult.AbsolutePath : uriResult.ToString();
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private JToken? MapOpenApiAnyToJToken(IOpenApiAny? any)
|
||||
{
|
||||
if (any == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using var outputString = new StringWriter();
|
||||
var writer = new OpenApiJsonWriter(outputString);
|
||||
any.Write(writer, OpenApiSpecVersion.OpenApi3_0);
|
||||
|
||||
if (any.AnyType == AnyType.Array)
|
||||
{
|
||||
return JArray.Parse(outputString.ToString());
|
||||
}
|
||||
|
||||
return JObject.Parse(outputString.ToString());
|
||||
}
|
||||
|
||||
private IDictionary<string, object>? MapHeaders(string? responseContentType, IDictionary<string, OpenApiHeader>? headers)
|
||||
{
|
||||
var mappedHeaders = headers?.ToDictionary(
|
||||
item => item.Key,
|
||||
_ => GetExampleMatcherModel(null, _settings.HeaderPatternToUse).Pattern!
|
||||
) ?? new Dictionary<string, object>();
|
||||
var mappedHeaders = headers?
|
||||
.ToDictionary(item => item.Key, _ => GetExampleMatcherModel(null, _settings.HeaderPatternToUse).Pattern!) ?? new Dictionary<string, object>();
|
||||
|
||||
if (!string.IsNullOrEmpty(responseContentType))
|
||||
{
|
||||
mappedHeaders.TryAdd(HeaderContentType, responseContentType!);
|
||||
mappedHeaders.TryAdd(HeaderContentType, responseContentType);
|
||||
}
|
||||
|
||||
return mappedHeaders.Keys.Any() ? mappedHeaders : null;
|
||||
}
|
||||
|
||||
private IList<ParamModel>? MapQueryParameters(IEnumerable<OpenApiParameter> queryParameters)
|
||||
private IList<ParamModel>? MapQueryParameters(IEnumerable<IOpenApiParameter> queryParameters)
|
||||
{
|
||||
var list = queryParameters
|
||||
.Where(req => req.Required)
|
||||
.Select(qp => new ParamModel
|
||||
{
|
||||
Name = qp.Name,
|
||||
Name = qp.Name ?? string.Empty,
|
||||
IgnoreCase = _settings.QueryParameterPatternIgnoreCase,
|
||||
Matchers = new[]
|
||||
{
|
||||
Matchers =
|
||||
[
|
||||
GetExampleMatcherModel(qp.Schema, _settings.QueryParameterPatternToUse)
|
||||
}
|
||||
]
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return list.Any() ? list : null;
|
||||
}
|
||||
|
||||
private IList<HeaderModel>? MapRequestHeaders(IEnumerable<OpenApiParameter> headers)
|
||||
private IList<HeaderModel>? MapRequestHeaders(IEnumerable<IOpenApiParameter> headers)
|
||||
{
|
||||
var list = headers
|
||||
.Where(req => req.Required)
|
||||
.Select(qp => new HeaderModel
|
||||
{
|
||||
Name = qp.Name,
|
||||
Name = qp.Name ?? string.Empty,
|
||||
IgnoreCase = _settings.HeaderPatternIgnoreCase,
|
||||
Matchers = new[]
|
||||
{
|
||||
Matchers =
|
||||
[
|
||||
GetExampleMatcherModel(qp.Schema, _settings.HeaderPatternToUse)
|
||||
}
|
||||
]
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return list.Any() ? list : null;
|
||||
}
|
||||
|
||||
private MatcherModel GetExampleMatcherModel(OpenApiSchema? schema, ExampleValueType type)
|
||||
private MatcherModel GetExampleMatcherModel(IOpenApiSchema? schema, ExampleValueType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
@@ -385,15 +317,31 @@ internal class OpenApiPathsMapper
|
||||
};
|
||||
}
|
||||
|
||||
private string GetExampleValueAsStringForSchemaType(OpenApiSchema? schema)
|
||||
private string GetExampleValueAsStringForSchemaType(IOpenApiSchema? schema)
|
||||
{
|
||||
var value = _exampleValueGenerator.GetExampleValue(schema);
|
||||
|
||||
return value switch
|
||||
if (value.GetValueKind() == JsonValueKind.String)
|
||||
{
|
||||
string valueAsString => valueAsString,
|
||||
return value.GetValue<string>();
|
||||
}
|
||||
|
||||
_ => value.ToString(),
|
||||
};
|
||||
return value.ToString();
|
||||
}
|
||||
|
||||
private static string MapBasePath(IList<OpenApiServer>? servers)
|
||||
{
|
||||
var server = servers?.FirstOrDefault();
|
||||
if (server == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (Uri.TryCreate(server.Url, UriKind.RelativeOrAbsolute, out var uriResult))
|
||||
{
|
||||
return uriResult.IsAbsoluteUri ? uriResult.AbsolutePath : uriResult.ToString();
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("WireMock.Net.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")]
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Microsoft.OpenApi.Models.Interfaces;
|
||||
|
||||
namespace WireMock.Net.OpenApiParser.Settings;
|
||||
|
||||
@@ -26,9 +26,9 @@ public interface IWireMockOpenApiParserExampleValues
|
||||
float Float { get; }
|
||||
|
||||
/// <summary>
|
||||
/// An example value for a Double.
|
||||
/// An example value for a Decimal.
|
||||
/// </summary>
|
||||
double Double { get; }
|
||||
decimal Decimal { get; }
|
||||
|
||||
/// <summary>
|
||||
/// An example value for a Date.
|
||||
@@ -58,5 +58,5 @@ public interface IWireMockOpenApiParserExampleValues
|
||||
/// <summary>
|
||||
/// OpenApi Schema to generate dynamic examples more accurate
|
||||
/// </summary>
|
||||
OpenApiSchema? Schema { get; set; }
|
||||
IOpenApiSchema? Schema { get; set; }
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Microsoft.OpenApi.Models.Interfaces;
|
||||
using RandomDataGenerator.FieldOptions;
|
||||
using RandomDataGenerator.Randomizers;
|
||||
|
||||
@@ -22,7 +22,7 @@ public class WireMockOpenApiParserDynamicExampleValues : IWireMockOpenApiParserE
|
||||
public virtual float Float => RandomizerFactory.GetRandomizer(new FieldOptionsFloat()).Generate() ?? 4.2f;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual double Double => RandomizerFactory.GetRandomizer(new FieldOptionsDouble()).Generate() ?? 4.2d;
|
||||
public virtual decimal Decimal => SafeConvertFloatToDecimal(RandomizerFactory.GetRandomizer(new FieldOptionsFloat()).Generate() ?? 4.2f);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Func<DateTime> Date => () => RandomizerFactory.GetRandomizer(new FieldOptionsDateTime()).Generate() ?? System.DateTime.UtcNow.Date;
|
||||
@@ -40,5 +40,20 @@ public class WireMockOpenApiParserDynamicExampleValues : IWireMockOpenApiParserE
|
||||
public virtual string String => RandomizerFactory.GetRandomizer(new FieldOptionsTextRegex { Pattern = @"^[0-9]{2}[A-Z]{5}[0-9]{2}" }).Generate() ?? "example-string";
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual OpenApiSchema? Schema { get; set; }
|
||||
public virtual IOpenApiSchema? Schema { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Safely converts a float to a decimal, ensuring the value stays within the bounds of a decimal.
|
||||
/// </summary>
|
||||
/// <param name="value">The float value to convert.</param>
|
||||
/// <returns>A decimal value within the valid range of a decimal.</returns>
|
||||
private static decimal SafeConvertFloatToDecimal(float value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
< (float)decimal.MinValue => decimal.MinValue,
|
||||
> (float)decimal.MaxValue => decimal.MaxValue,
|
||||
_ => (decimal)value
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
using System;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Microsoft.OpenApi.Models.Interfaces;
|
||||
|
||||
namespace WireMock.Net.OpenApiParser.Settings;
|
||||
|
||||
@@ -20,7 +21,7 @@ public class WireMockOpenApiParserExampleValues : IWireMockOpenApiParserExampleV
|
||||
public virtual float Float => 4.2f;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual double Double => 4.2d;
|
||||
public virtual decimal Decimal => 4.2m;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Func<DateTime> Date { get; } = () => System.DateTime.UtcNow.Date;
|
||||
@@ -29,7 +30,7 @@ public class WireMockOpenApiParserExampleValues : IWireMockOpenApiParserExampleV
|
||||
public virtual Func<DateTime> DateTime { get; } = () => System.DateTime.UtcNow;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual byte[] Bytes { get; } = { 48, 49, 50 };
|
||||
public virtual byte[] Bytes { get; } = [48, 49, 50];
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual object Object => "example-object";
|
||||
@@ -38,5 +39,5 @@ public class WireMockOpenApiParserExampleValues : IWireMockOpenApiParserExampleV
|
||||
public virtual string String => "example-string";
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual OpenApiSchema? Schema { get; set; } = new();
|
||||
public virtual IOpenApiSchema? Schema { get; set; } = new OpenApiSchema();
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
namespace WireMock.Net.OpenApiParser.Types;
|
||||
|
||||
internal enum SchemaType
|
||||
{
|
||||
Object,
|
||||
|
||||
Array,
|
||||
|
||||
String,
|
||||
|
||||
Integer,
|
||||
|
||||
Number,
|
||||
|
||||
Boolean,
|
||||
|
||||
File,
|
||||
|
||||
Unknown
|
||||
}
|
||||
@@ -7,13 +7,16 @@ namespace WireMock.Net.OpenApiParser.Utils;
|
||||
|
||||
internal static class DateTimeUtils
|
||||
{
|
||||
private const string DateFormat = "yyyy-MM-dd";
|
||||
private const string DateTimeFormat = "yyyy-MM-dd'T'HH:mm:ss.fffzzz";
|
||||
|
||||
public static string ToRfc3339DateTime(DateTime dateTime)
|
||||
{
|
||||
return dateTime.ToString("yyyy-MM-dd'T'HH:mm:ss.fffzzz", DateTimeFormatInfo.InvariantInfo);
|
||||
return dateTime.ToString(DateTimeFormat, DateTimeFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
public static string ToRfc3339Date(DateTime dateTime)
|
||||
{
|
||||
return dateTime.ToString("yyyy-MM-dd", DateTimeFormatInfo.InvariantInfo);
|
||||
return dateTime.ToString(DateFormat, DateTimeFormatInfo.InvariantInfo);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Microsoft.OpenApi.Models.Interfaces;
|
||||
using Stef.Validation;
|
||||
using WireMock.Net.OpenApiParser.Extensions;
|
||||
using WireMock.Net.OpenApiParser.Settings;
|
||||
@@ -36,82 +38,66 @@ internal class ExampleValueGenerator
|
||||
}
|
||||
}
|
||||
|
||||
public object GetExampleValue(OpenApiSchema? schema)
|
||||
public JsonNode GetExampleValue(IOpenApiSchema? schema)
|
||||
{
|
||||
var schemaExample = schema?.Example;
|
||||
var schemaEnum = schema?.Enum?.FirstOrDefault();
|
||||
|
||||
_exampleValues.Schema = schema;
|
||||
|
||||
switch (schema?.GetSchemaType())
|
||||
switch (schema?.GetSchemaType(out _))
|
||||
{
|
||||
case SchemaType.Boolean:
|
||||
var exampleBoolean = schemaExample as OpenApiBoolean;
|
||||
return exampleBoolean?.Value ?? _exampleValues.Boolean;
|
||||
case JsonSchemaType.Boolean:
|
||||
var exampleBoolean = schemaExample?.GetValue<bool>();
|
||||
return exampleBoolean ?? _exampleValues.Boolean;
|
||||
|
||||
case SchemaType.Integer:
|
||||
switch (schema?.GetSchemaFormat())
|
||||
{
|
||||
case SchemaFormat.Int64:
|
||||
var exampleLong = schemaExample as OpenApiLong;
|
||||
var enumLong = schemaEnum as OpenApiLong;
|
||||
var valueLongEnumOrExample = enumLong?.Value ?? exampleLong?.Value;
|
||||
return valueLongEnumOrExample ?? _exampleValues.Integer;
|
||||
case JsonSchemaType.Integer:
|
||||
var exampleInteger = schemaExample?.GetValue<decimal>();
|
||||
var enumInteger = schemaEnum?.GetValue<decimal>();
|
||||
var valueIntegerEnumOrExample = enumInteger ?? exampleInteger;
|
||||
return valueIntegerEnumOrExample ?? _exampleValues.Integer;
|
||||
|
||||
default:
|
||||
var exampleInteger = schemaExample as OpenApiInteger;
|
||||
var enumInteger = schemaEnum as OpenApiInteger;
|
||||
var valueIntegerEnumOrExample = enumInteger?.Value ?? exampleInteger?.Value;
|
||||
return valueIntegerEnumOrExample ?? _exampleValues.Integer;
|
||||
}
|
||||
|
||||
case SchemaType.Number:
|
||||
switch (schema?.GetSchemaFormat())
|
||||
case JsonSchemaType.Number:
|
||||
switch (schema.GetSchemaFormat())
|
||||
{
|
||||
case SchemaFormat.Float:
|
||||
var exampleFloat = schemaExample as OpenApiFloat;
|
||||
var enumFloat = schemaEnum as OpenApiFloat;
|
||||
var valueFloatEnumOrExample = enumFloat?.Value ?? exampleFloat?.Value;
|
||||
var exampleFloat = schemaExample?.GetValue<float>();
|
||||
var enumFloat = schemaEnum?.GetValue<float>();
|
||||
var valueFloatEnumOrExample = enumFloat ?? exampleFloat;
|
||||
return valueFloatEnumOrExample ?? _exampleValues.Float;
|
||||
|
||||
default:
|
||||
var exampleDouble = schemaExample as OpenApiDouble;
|
||||
var enumDouble = schemaEnum as OpenApiDouble;
|
||||
var valueDoubleEnumOrExample = enumDouble?.Value ?? exampleDouble?.Value;
|
||||
return valueDoubleEnumOrExample ?? _exampleValues.Double;
|
||||
var exampleDecimal = schemaExample?.GetValue<decimal>();
|
||||
var enumDecimal = schemaEnum?.GetValue<decimal>();
|
||||
var valueDecimalEnumOrExample = enumDecimal ?? exampleDecimal;
|
||||
return valueDecimalEnumOrExample ?? _exampleValues.Decimal;
|
||||
}
|
||||
|
||||
default:
|
||||
switch (schema?.GetSchemaFormat())
|
||||
{
|
||||
case SchemaFormat.Date:
|
||||
var exampleDate = schemaExample as OpenApiDate;
|
||||
var enumDate = schemaEnum as OpenApiDate;
|
||||
var valueDateEnumOrExample = enumDate?.Value ?? exampleDate?.Value;
|
||||
return DateTimeUtils.ToRfc3339Date(valueDateEnumOrExample ?? _exampleValues.Date());
|
||||
var exampleDate = schemaExample?.GetValue<string>();
|
||||
var enumDate = schemaEnum?.GetValue<string>();
|
||||
var valueDateEnumOrExample = enumDate ?? exampleDate;
|
||||
return valueDateEnumOrExample ?? DateTimeUtils.ToRfc3339Date(_exampleValues.Date());
|
||||
|
||||
case SchemaFormat.DateTime:
|
||||
var exampleDateTime = schemaExample as OpenApiDateTime;
|
||||
var enumDateTime = schemaEnum as OpenApiDateTime;
|
||||
var valueDateTimeEnumOrExample = enumDateTime?.Value ?? exampleDateTime?.Value;
|
||||
return DateTimeUtils.ToRfc3339DateTime(valueDateTimeEnumOrExample?.DateTime ?? _exampleValues.DateTime());
|
||||
var exampleDateTime = schemaExample?.GetValue<string>();
|
||||
var enumDateTime = schemaEnum?.GetValue<string>();
|
||||
var valueDateTimeEnumOrExample = enumDateTime ?? exampleDateTime;
|
||||
return valueDateTimeEnumOrExample ?? DateTimeUtils.ToRfc3339DateTime(_exampleValues.DateTime());
|
||||
|
||||
case SchemaFormat.Byte:
|
||||
var exampleByte = schemaExample as OpenApiByte;
|
||||
var enumByte = schemaEnum as OpenApiByte;
|
||||
var valueByteEnumOrExample = enumByte?.Value ?? exampleByte?.Value;
|
||||
return valueByteEnumOrExample ?? _exampleValues.Bytes;
|
||||
|
||||
case SchemaFormat.Binary:
|
||||
var exampleBinary = schemaExample as OpenApiBinary;
|
||||
var enumBinary = schemaEnum as OpenApiBinary;
|
||||
var valueBinaryEnumOrExample = enumBinary?.Value ?? exampleBinary?.Value;
|
||||
return valueBinaryEnumOrExample ?? _exampleValues.Object;
|
||||
var exampleByte = schemaExample?.GetValue<byte[]>();
|
||||
var enumByte = schemaEnum?.GetValue<byte[]>();
|
||||
var valueByteEnumOrExample = enumByte ?? exampleByte;
|
||||
return Convert.ToBase64String(valueByteEnumOrExample ?? _exampleValues.Bytes);
|
||||
|
||||
default:
|
||||
var exampleString = schemaExample as OpenApiString;
|
||||
var enumString = schemaEnum as OpenApiString;
|
||||
var valueStringEnumOrExample = enumString?.Value ?? exampleString?.Value;
|
||||
var exampleString = schemaExample?.GetValue<string>();
|
||||
var enumString = schemaEnum?.GetValue<string>();
|
||||
var valueStringEnumOrExample = enumString ?? exampleString;
|
||||
return valueStringEnumOrExample ?? _exampleValues.String;
|
||||
}
|
||||
}
|
||||
|
||||
27
src/WireMock.Net.OpenApiParser/Utils/PathUtils.cs
Normal file
27
src/WireMock.Net.OpenApiParser/Utils/PathUtils.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
namespace WireMock.Net.OpenApiParser.Utils;
|
||||
|
||||
internal static class PathUtils
|
||||
{
|
||||
internal static string Combine(params string[] paths)
|
||||
{
|
||||
if (paths.Length == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var result = paths[0].Trim().TrimEnd('/');
|
||||
|
||||
for (int i = 1; i < paths.Length; i++)
|
||||
{
|
||||
var nextPath = paths[i].Trim().TrimStart('/').TrimEnd('/');
|
||||
if (!string.IsNullOrEmpty(nextPath))
|
||||
{
|
||||
result += '/' + nextPath;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>An OpenApi (swagger) parser to generate MappingModel or mapping.json file.</Description>
|
||||
<TargetFrameworks>net46;netstandard2.0;netstandard2.1</TargetFrameworks>
|
||||
<TargetFrameworks>net47;netstandard2.0;netstandard2.1;net8.0</TargetFrameworks>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageTags>wiremock;openapi;OAS;raml;converter;parser;openapiparser</PackageTags>
|
||||
<ProjectGuid>{D3804228-91F4-4502-9595-39584E5AADAD}</ProjectGuid>
|
||||
@@ -20,12 +20,11 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.2.3" />
|
||||
<PackageReference Include="Nullable" Version="1.3.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="RamlToOpenApiConverter" Version="0.6.1" />
|
||||
<PackageReference Include="RamlToOpenApiConverter" Version="0.7.0" />
|
||||
<PackageReference Include="RandomDataGenerator.Net" Version="1.0.18" />
|
||||
<PackageReference Include="Stef.Validation" Version="0.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -6,7 +6,8 @@ using System.IO;
|
||||
using System.Text;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Microsoft.OpenApi.Readers;
|
||||
using Microsoft.OpenApi.Reader;
|
||||
using Microsoft.OpenApi.YamlReader;
|
||||
using RamlToOpenApiConverter;
|
||||
using WireMock.Admin.Mappings;
|
||||
using WireMock.Net.OpenApiParser.Mappers;
|
||||
@@ -19,7 +20,7 @@ namespace WireMock.Net.OpenApiParser;
|
||||
/// </summary>
|
||||
public class WireMockOpenApiParser : IWireMockOpenApiParser
|
||||
{
|
||||
private readonly OpenApiStreamReader _reader = new();
|
||||
private static readonly OpenApiReaderSettings ReaderSettings = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
[PublicAPI]
|
||||
@@ -40,8 +41,7 @@ public class WireMockOpenApiParser : IWireMockOpenApiParser
|
||||
}
|
||||
else
|
||||
{
|
||||
var reader = new OpenApiStreamReader();
|
||||
document = reader.Read(File.OpenRead(path), out diagnostic);
|
||||
document = Read(File.OpenRead(path), out diagnostic);
|
||||
}
|
||||
|
||||
return FromDocument(document, settings);
|
||||
@@ -51,21 +51,21 @@ public class WireMockOpenApiParser : IWireMockOpenApiParser
|
||||
[PublicAPI]
|
||||
public IReadOnlyList<MappingModel> FromDocument(OpenApiDocument document, WireMockOpenApiParserSettings? settings = null)
|
||||
{
|
||||
return new OpenApiPathsMapper(settings ?? new WireMockOpenApiParserSettings()).ToMappingModels(document.Paths, document.Servers);
|
||||
return new OpenApiPathsMapper(settings ?? new WireMockOpenApiParserSettings()).ToMappingModels(document.Paths, document.Servers ?? []);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[PublicAPI]
|
||||
public IReadOnlyList<MappingModel> FromStream(Stream stream, out OpenApiDiagnostic diagnostic)
|
||||
{
|
||||
return FromDocument(_reader.Read(stream, out diagnostic));
|
||||
return FromDocument(Read(stream, out diagnostic));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[PublicAPI]
|
||||
public IReadOnlyList<MappingModel> FromStream(Stream stream, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic)
|
||||
{
|
||||
return FromDocument(_reader.Read(stream, out diagnostic), settings);
|
||||
return FromDocument(Read(stream, out diagnostic), settings);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -81,4 +81,27 @@ public class WireMockOpenApiParser : IWireMockOpenApiParser
|
||||
{
|
||||
return FromStream(new MemoryStream(Encoding.UTF8.GetBytes(text)), settings, out diagnostic);
|
||||
}
|
||||
|
||||
private static OpenApiDocument Read(Stream stream, out OpenApiDiagnostic diagnostic)
|
||||
{
|
||||
var reader = new OpenApiYamlReader();
|
||||
|
||||
if (stream is not MemoryStream memoryStream)
|
||||
{
|
||||
memoryStream = ReadStreamIntoMemoryStream(stream);
|
||||
}
|
||||
|
||||
var result = reader.Read(memoryStream, ReaderSettings);
|
||||
|
||||
diagnostic = result.Diagnostic ?? new OpenApiDiagnostic();
|
||||
return result.Document ?? throw new InvalidOperationException("The document is null.");
|
||||
}
|
||||
|
||||
private static MemoryStream ReadStreamIntoMemoryStream(Stream stream)
|
||||
{
|
||||
var memoryStream = new MemoryStream();
|
||||
stream.CopyTo(memoryStream);
|
||||
memoryStream.Position = 0;
|
||||
return memoryStream;
|
||||
}
|
||||
}
|
||||
31
src/WireMock.Net/Extensions/StringExtensions.cs
Normal file
31
src/WireMock.Net/Extensions/StringExtensions.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace WireMock.Extensions;
|
||||
|
||||
internal static class StringExtensions
|
||||
{
|
||||
// See https://andrewlock.net/why-is-string-gethashcode-different-each-time-i-run-my-program-in-net-core/
|
||||
public static string GetDeterministicHashCodeAsString(this string str)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int hash1 = (5381 << 16) + 5381;
|
||||
int hash2 = hash1;
|
||||
|
||||
for (int i = 0; i < str.Length; i += 2)
|
||||
{
|
||||
hash1 = ((hash1 << 5) + hash1) ^ str[i];
|
||||
if (i == str.Length - 1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
hash2 = ((hash2 << 5) + hash2) ^ str[i + 1];
|
||||
}
|
||||
|
||||
int result = hash1 + hash2 * 1566083941;
|
||||
|
||||
return result.ToString(CultureInfo.InvariantCulture).Replace('-', '_');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,20 +84,20 @@ public class LocalFileSystemHandler : IFileSystemHandler
|
||||
public virtual byte[] ReadResponseBodyAsFile(string path)
|
||||
{
|
||||
Guard.NotNullOrEmpty(path);
|
||||
path = PathUtils.CleanPath(path)!;
|
||||
path = FilePathUtils.CleanPath(path)!;
|
||||
// If the file exists at the given path relative to the MappingsFolder, then return that.
|
||||
// Else the path will just be as-is.
|
||||
return File.ReadAllBytes(File.Exists(PathUtils.Combine(GetMappingFolder(), path)) ? PathUtils.Combine(GetMappingFolder(), path) : path);
|
||||
return File.ReadAllBytes(File.Exists(FilePathUtils.Combine(GetMappingFolder(), path)) ? FilePathUtils.Combine(GetMappingFolder(), path) : path);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IFileSystemHandler.ReadResponseBodyAsString"/>
|
||||
public virtual string ReadResponseBodyAsString(string path)
|
||||
{
|
||||
Guard.NotNullOrEmpty(path);
|
||||
path = PathUtils.CleanPath(path)!;
|
||||
path = FilePathUtils.CleanPath(path)!;
|
||||
// In case the path is a filename, the path will be adjusted to the MappingFolder.
|
||||
// Else the path will just be as-is.
|
||||
return File.ReadAllText(File.Exists(PathUtils.Combine(GetMappingFolder(), path)) ? PathUtils.Combine(GetMappingFolder(), path) : path);
|
||||
return File.ReadAllText(File.Exists(FilePathUtils.Combine(GetMappingFolder(), path)) ? FilePathUtils.Combine(GetMappingFolder(), path) : path);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IFileSystemHandler.FileExists"/>
|
||||
@@ -124,7 +124,7 @@ public class LocalFileSystemHandler : IFileSystemHandler
|
||||
Guard.NotNullOrEmpty(filename);
|
||||
Guard.NotNull(bytes);
|
||||
|
||||
File.WriteAllBytes(PathUtils.Combine(folder, filename), bytes);
|
||||
File.WriteAllBytes(FilePathUtils.Combine(folder, filename), bytes);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IFileSystemHandler.DeleteFile"/>
|
||||
|
||||
@@ -67,7 +67,7 @@ public class JsonPathMatcher : IStringMatcher, IObjectMatcher
|
||||
var score = MatchScores.Mismatch;
|
||||
Exception? exception = null;
|
||||
|
||||
if (input != null)
|
||||
if (!string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -74,7 +74,7 @@ public class JmesPathMatcher : IStringMatcher, IObjectMatcher
|
||||
var score = MatchScores.Mismatch;
|
||||
Exception? exception = null;
|
||||
|
||||
if (input != null)
|
||||
if (!string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
85
src/WireMock.Net/Models/BlockingQueue.cs
Normal file
85
src/WireMock.Net/Models/BlockingQueue.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
|
||||
namespace WireMock.Models;
|
||||
|
||||
/// <inheritdoc />
|
||||
internal class BlockingQueue<T>(TimeSpan? readTimeout = null) : IBlockingQueue<T>
|
||||
{
|
||||
private readonly TimeSpan _readTimeout = readTimeout ?? TimeSpan.FromHours(1);
|
||||
private readonly Queue<T?> _queue = new();
|
||||
private readonly object _lockObject = new();
|
||||
|
||||
private bool _isClosed;
|
||||
|
||||
/// <summary>
|
||||
/// Writes an item to the queue and signals that an item is available.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to be added to the queue.</param>
|
||||
public void Write(T item)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (_isClosed)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot write to a closed queue.");
|
||||
}
|
||||
|
||||
_queue.Enqueue(item);
|
||||
|
||||
// Signal that an item is available
|
||||
Monitor.Pulse(_lockObject);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to read an item from the queue.
|
||||
/// - waits until an item is available
|
||||
/// - or the timeout occurs
|
||||
/// - or queue is closed
|
||||
/// </summary>
|
||||
/// <param name="item">The item read from the queue, or default if the timeout occurs.</param>
|
||||
/// <returns>True if an item was successfully read; otherwise, false.</returns>
|
||||
public bool TryRead([NotNullWhen(true)] out T? item)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
// Wait until an item is available or timeout occurs
|
||||
while (_queue.Count == 0 && !_isClosed)
|
||||
{
|
||||
// Wait with timeout
|
||||
if (!Monitor.Wait(_lockObject, _readTimeout))
|
||||
{
|
||||
item = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// After waiting, check if we have items
|
||||
if (_queue.Count == 0)
|
||||
{
|
||||
item = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
item = _queue.Dequeue();
|
||||
return item != null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the queue and signals all waiting threads.
|
||||
/// </summary>
|
||||
public void Close()
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
_isClosed = true;
|
||||
Monitor.PulseAll(_lockObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using WireMock.Models;
|
||||
using WireMock.Types;
|
||||
|
||||
@@ -57,4 +58,10 @@ public class BodyData : IBodyData
|
||||
/// <inheritdoc />
|
||||
public string? ProtoBufMessageType { get; set; }
|
||||
#endregion
|
||||
|
||||
/// <inheritdoc />
|
||||
public IBlockingQueue<string?>? SseStringQueue { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task? BodyAsSseStringTask { get; set; }
|
||||
}
|
||||
39
src/WireMock.Net/Models/ProtoDefinitionData.cs
Normal file
39
src/WireMock.Net/Models/ProtoDefinitionData.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Stef.Validation;
|
||||
|
||||
namespace WireMock.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A placeholder class for Proto Definitions.
|
||||
/// </summary>
|
||||
public class ProtoDefinitionData
|
||||
{
|
||||
private readonly IDictionary<string, string> _filenameMappedToProtoDefinition;
|
||||
|
||||
internal ProtoDefinitionData(IDictionary<string, string> filenameMappedToProtoDefinition)
|
||||
{
|
||||
_filenameMappedToProtoDefinition = filenameMappedToProtoDefinition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all the ProtoDefinitions.
|
||||
/// Note: the main ProtoDefinition will be the first one in the list.
|
||||
/// </summary>
|
||||
/// <param name="mainProtoFilename">The main ProtoDefinition filename.</param>
|
||||
public IReadOnlyList<string> ToList(string mainProtoFilename)
|
||||
{
|
||||
Guard.NotNullOrEmpty(mainProtoFilename);
|
||||
|
||||
if (!_filenameMappedToProtoDefinition.TryGetValue(mainProtoFilename, out var mainProtoDefinition))
|
||||
{
|
||||
throw new KeyNotFoundException($"The ProtoDefinition with filename '{mainProtoFilename}' was not found.");
|
||||
}
|
||||
|
||||
var list = new List<string> { mainProtoDefinition };
|
||||
list.AddRange(_filenameMappedToProtoDefinition.Where(kvp => kvp.Key != mainProtoFilename).Select(kvp => kvp.Value));
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,13 @@ namespace WireMock.Owin.Mappers
|
||||
return;
|
||||
}
|
||||
|
||||
var bodyData = responseMessage.BodyData;
|
||||
if (bodyData?.GetBodyType() == BodyType.SseString)
|
||||
{
|
||||
await HandleSseStringAsync(responseMessage, response, bodyData);
|
||||
return;
|
||||
}
|
||||
|
||||
byte[]? bytes;
|
||||
switch (responseMessage.FaultType)
|
||||
{
|
||||
@@ -104,7 +111,7 @@ namespace WireMock.Owin.Mappers
|
||||
}
|
||||
}
|
||||
|
||||
SetResponseHeaders(responseMessage, bytes, response);
|
||||
SetResponseHeaders(responseMessage, bytes != null, response);
|
||||
|
||||
if (bytes != null)
|
||||
{
|
||||
@@ -121,6 +128,26 @@ namespace WireMock.Owin.Mappers
|
||||
SetResponseTrailingHeaders(responseMessage, response);
|
||||
}
|
||||
|
||||
private static async Task HandleSseStringAsync(IResponseMessage responseMessage, IResponse response, IBodyData bodyData)
|
||||
{
|
||||
if (bodyData.SseStringQueue == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetResponseHeaders(responseMessage, true, response);
|
||||
|
||||
string? text;
|
||||
do
|
||||
{
|
||||
if (bodyData.SseStringQueue.TryRead(out text))
|
||||
{
|
||||
await response.WriteAsync(text);
|
||||
await response.Body.FlushAsync();
|
||||
}
|
||||
} while (text != null);
|
||||
}
|
||||
|
||||
private int MapStatusCode(int code)
|
||||
{
|
||||
if (_options.AllowOnlyDefinedHttpStatusCodeInResponse == true && !Enum.IsDefined(typeof(HttpStatusCode), code))
|
||||
@@ -136,7 +163,8 @@ namespace WireMock.Owin.Mappers
|
||||
return responseMessage.FaultPercentage == null || _randomizerDouble.Generate() <= responseMessage.FaultPercentage;
|
||||
}
|
||||
|
||||
private async Task<byte[]?> GetNormalBodyAsync(IResponseMessage responseMessage) {
|
||||
private async Task<byte[]?> GetNormalBodyAsync(IResponseMessage responseMessage)
|
||||
{
|
||||
var bodyData = responseMessage.BodyData;
|
||||
switch (bodyData?.GetBodyType())
|
||||
{
|
||||
@@ -172,13 +200,13 @@ namespace WireMock.Owin.Mappers
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void SetResponseHeaders(IResponseMessage responseMessage, byte[]? bytes, IResponse response)
|
||||
private static void SetResponseHeaders(IResponseMessage responseMessage, bool hasBody, IResponse response)
|
||||
{
|
||||
// Force setting the Date header (#577)
|
||||
AppendResponseHeader(
|
||||
response,
|
||||
HttpKnownHeaderNames.Date,
|
||||
[ DateTime.UtcNow.ToString(CultureInfo.InvariantCulture.DateTimeFormat.RFC1123Pattern, CultureInfo.InvariantCulture) ]
|
||||
[DateTime.UtcNow.ToString(CultureInfo.InvariantCulture.DateTimeFormat.RFC1123Pattern, CultureInfo.InvariantCulture)]
|
||||
);
|
||||
|
||||
// Set other headers
|
||||
@@ -188,7 +216,7 @@ namespace WireMock.Owin.Mappers
|
||||
var value = item.Value;
|
||||
if (ResponseHeadersToFix.TryGetValue(headerName, out var action))
|
||||
{
|
||||
action?.Invoke(response, bytes != null, value);
|
||||
action?.Invoke(response, hasBody, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -37,13 +37,14 @@ public partial class Request
|
||||
/// <inheritdoc />
|
||||
public IRequestBuilder WithBodyAsJson(object body, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
|
||||
{
|
||||
return WithBody(new IMatcher[] { new JsonMatcher(matchBehaviour, body) });
|
||||
var matcher = body as IMatcher ?? new JsonMatcher(matchBehaviour, body);
|
||||
return WithBody([matcher]);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRequestBuilder WithBody(IMatcher matcher)
|
||||
{
|
||||
return WithBody(new[] { matcher });
|
||||
return WithBody([matcher]);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using WireMock.Matchers;
|
||||
using WireMock.Matchers.Request;
|
||||
@@ -12,7 +13,7 @@ public partial class Request
|
||||
/// <inheritdoc />
|
||||
public IRequestBuilder WithBodyAsProtoBuf(string protoDefinition, string messageType, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
|
||||
{
|
||||
return WithBodyAsProtoBuf([ protoDefinition ], messageType, matchBehaviour);
|
||||
return WithBodyAsProtoBuf([protoDefinition], messageType, matchBehaviour);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -36,12 +37,25 @@ public partial class Request
|
||||
/// <inheritdoc />
|
||||
public IRequestBuilder WithBodyAsProtoBuf(string messageType, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
|
||||
{
|
||||
return Add(new RequestMessageProtoBufMatcher(matchBehaviour, () => Mapping.ProtoDefinition!.Value, messageType));
|
||||
return Add(new RequestMessageProtoBufMatcher(matchBehaviour, ProtoDefinitionFunc(), messageType));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRequestBuilder WithBodyAsProtoBuf(string messageType, IObjectMatcher matcher, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
|
||||
{
|
||||
return Add(new RequestMessageProtoBufMatcher(matchBehaviour, () => Mapping.ProtoDefinition!.Value, messageType, matcher));
|
||||
return Add(new RequestMessageProtoBufMatcher(matchBehaviour, ProtoDefinitionFunc(), messageType, matcher));
|
||||
}
|
||||
|
||||
private Func<IdOrTexts> ProtoDefinitionFunc()
|
||||
{
|
||||
return () =>
|
||||
{
|
||||
if (Mapping.ProtoDefinition == null)
|
||||
{
|
||||
throw new InvalidOperationException($"No ProtoDefinition defined on mapping '{Mapping.Guid}'. Please use the WireMockServerSettings to define ProtoDefinitions.");
|
||||
}
|
||||
|
||||
return Mapping.ProtoDefinition.Value;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using JsonConverter.Abstractions;
|
||||
using WireMock.Models;
|
||||
|
||||
namespace WireMock.ResponseBuilders;
|
||||
|
||||
@@ -32,7 +33,7 @@ public interface IBodyResponseBuilder : IFaultResponseBuilder
|
||||
IResponseBuilder WithBody(Func<IRequestMessage, string> bodyFactory, string? destination = BodyDestinationFormat.SameAsSource, Encoding? encoding = null);
|
||||
|
||||
/// <summary>
|
||||
/// WithBody : Create a ... response based on a async callback function.
|
||||
/// WithBody : Create a ... response based on an async callback function.
|
||||
/// </summary>
|
||||
/// <param name="bodyFactory">The async delegate to build the body.</param>
|
||||
/// <param name="destination">The Body Destination format (SameAsSource, String or Bytes).</param>
|
||||
@@ -40,6 +41,14 @@ public interface IBodyResponseBuilder : IFaultResponseBuilder
|
||||
/// <returns>A <see cref="IResponseBuilder"/>.</returns>
|
||||
IResponseBuilder WithBody(Func<IRequestMessage, Task<string>> bodyFactory, string? destination = BodyDestinationFormat.SameAsSource, Encoding? encoding = null);
|
||||
|
||||
/// <summary>
|
||||
/// WithBody : Create a ... response based on an async callback function.
|
||||
/// </summary>
|
||||
/// <param name="bodyFactory">The async delegate to build the body.</param>
|
||||
/// <param name="timeout">The timeout to wait on new items in the queue. Default value is <c>1</c> hour.</param>
|
||||
/// <returns>A <see cref="IResponseBuilder"/>.</returns>
|
||||
IResponseBuilder WithSseBody(Func<IRequestMessage, IBlockingQueue<string?>, Task> bodyFactory, TimeSpan? timeout = null);
|
||||
|
||||
/// <summary>
|
||||
/// WithBody : Create a ... response based on a bytearray.
|
||||
/// </summary>
|
||||
|
||||
@@ -51,6 +51,26 @@ public partial class Response
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IResponseBuilder WithSseBody(Func<IRequestMessage, IBlockingQueue<string?>, Task> bodyFactory, TimeSpan? timeout = null)
|
||||
{
|
||||
Guard.NotNull(bodyFactory);
|
||||
|
||||
var queue = new BlockingQueue<string?>(timeout);
|
||||
|
||||
return WithCallbackInternal(true, req => new ResponseMessage
|
||||
{
|
||||
BodyData = new BodyData
|
||||
{
|
||||
DetectedBodyType = BodyType.SseString,
|
||||
SseStringQueue = queue,
|
||||
BodyAsSseStringTask = bodyFactory(req, queue),
|
||||
Encoding = Encoding.UTF8,
|
||||
IsFuncUsed = "Func<IRequestMessage, BlockingQueue<string?>, Task>"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IResponseBuilder WithBody(byte[] body, string? destination = BodyDestinationFormat.SameAsSource, Encoding? encoding = null)
|
||||
{
|
||||
|
||||
@@ -356,9 +356,12 @@ internal class RespondWithAProvider : IRespondWithAProvider
|
||||
{
|
||||
Guard.NotNull(protoDefinitionOrId);
|
||||
|
||||
#if PROTOBUF
|
||||
ProtoDefinition = ProtoDefinitionHelper.GetIdOrTexts(_settings, protoDefinitionOrId);
|
||||
|
||||
return this;
|
||||
#else
|
||||
throw new NotSupportedException("The WithProtoDefinition method can not be used for .NETStandard1.3 or .NET Framework 4.6.1 or lower.");
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -25,7 +25,7 @@ public partial class WireMockServer
|
||||
return ResponseMessageBuilder.Create(HttpStatusCode.BadRequest, e.Message);
|
||||
}
|
||||
#else
|
||||
return ResponseMessageBuilder.Create(HttpStatusCode.BadRequest, "Not supported for .NETStandard 1.3 and .NET 4.5.2 or lower.");
|
||||
return ResponseMessageBuilder.Create(HttpStatusCode.BadRequest, "Not supported for .NETStandard 1.3 and .NET 4.6.x or lower.");
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ public partial class WireMockServer
|
||||
return ResponseMessageBuilder.Create(HttpStatusCode.BadRequest, e.Message);
|
||||
}
|
||||
#else
|
||||
return ResponseMessageBuilder.Create(HttpStatusCode.BadRequest, "Not supported for .NETStandard 1.3 and .NET 4.5.2 or lower.");
|
||||
return ResponseMessageBuilder.Create(HttpStatusCode.BadRequest, "Not supported for .NETStandard 1.3 and .NET 4.6.x or lower.");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
50
src/WireMock.Net/Settings/HandlebarsSettings.cs
Normal file
50
src/WireMock.Net/Settings/HandlebarsSettings.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using HandlebarsDotNet.Helpers.Enums;
|
||||
using JetBrains.Annotations;
|
||||
using WireMock.Types;
|
||||
|
||||
namespace WireMock.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// HandlebarsSettings
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public class HandlebarsSettings
|
||||
{
|
||||
internal static readonly Category[] DefaultAllowedHandlebarsHelpers =
|
||||
[
|
||||
Category.Boolean,
|
||||
Category.Constants,
|
||||
Category.DateTime,
|
||||
Category.Enumerable,
|
||||
Category.Humanizer,
|
||||
Category.JsonPath,
|
||||
Category.Math,
|
||||
Category.Object,
|
||||
Category.Random,
|
||||
Category.Regex,
|
||||
Category.String,
|
||||
Category.Url,
|
||||
Category.Xeger,
|
||||
Category.XPath,
|
||||
Category.Xslt
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Defines the allowed custom HandlebarsHelpers which can be used. Possible values are:
|
||||
/// - <see cref="CustomHandlebarsHelpers.None"/> (Default)
|
||||
/// - <see cref="CustomHandlebarsHelpers.File"/>
|
||||
/// - <see cref="CustomHandlebarsHelpers.All"/>
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public CustomHandlebarsHelpers AllowedCustomHandlebarsHelpers { get; set; } = CustomHandlebarsHelpers.None;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the allowed HandlebarHelpers which can be used.
|
||||
///
|
||||
/// By default, all categories except <see cref="Category.DynamicLinq"/> and <see cref="Category.Environment"/> are registered.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public Category[] AllowedHandlebarsHelpers { get; set; } = DefaultAllowedHandlebarsHelpers;
|
||||
}
|
||||
@@ -98,7 +98,7 @@ public class ProxyAndRecordSettings : HttpClientSettings
|
||||
public bool UseDefinedRequestMatchers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Append an unique GUID to the filename from the saved mapping file.
|
||||
/// Append a unique GUID to the filename from the saved mapping file.
|
||||
/// </summary>
|
||||
public bool AppendGuidToSavedMappingFile { get; set; }
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ public class ProxyUrlReplaceSettings
|
||||
public string NewValue { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Defines if the case should be ignore when replacing.
|
||||
/// Defines if the case should be ignored when replacing.
|
||||
/// </summary>
|
||||
public bool IgnoreCase { get; set; }
|
||||
}
|
||||
@@ -70,6 +70,11 @@ internal class SimpleSettingsParser
|
||||
return Arguments.ContainsKey(name);
|
||||
}
|
||||
|
||||
public bool ContainsAny(params string[] names)
|
||||
{
|
||||
return names.Any(Arguments.ContainsKey);
|
||||
}
|
||||
|
||||
public string[] GetValues(string name, string[] defaultValue)
|
||||
{
|
||||
return Contains(name) ? Arguments[name] : defaultValue;
|
||||
@@ -142,6 +147,28 @@ internal class SimpleSettingsParser
|
||||
}, defaultValue);
|
||||
}
|
||||
|
||||
public TEnum[] GetEnumValues<TEnum>(string name, TEnum[] defaultValues)
|
||||
where TEnum : struct
|
||||
{
|
||||
var values = GetValues(name);
|
||||
if (values == null)
|
||||
{
|
||||
return defaultValues;
|
||||
}
|
||||
|
||||
var enums = new List<TEnum>();
|
||||
|
||||
foreach (var value in values)
|
||||
{
|
||||
if (Enum.TryParse<TEnum>(value, true, out var enumValue))
|
||||
{
|
||||
enums.Add(enumValue);
|
||||
}
|
||||
}
|
||||
|
||||
return enums.ToArray();
|
||||
}
|
||||
|
||||
public string GetStringValue(string name, string defaultValue)
|
||||
{
|
||||
return GetValue(name, values => values.FirstOrDefault() ?? defaultValue, defaultValue);
|
||||
|
||||
@@ -16,13 +16,13 @@ public class WebProxySettings
|
||||
public string Address { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The user name associated with the credentials.
|
||||
/// The username associated with the credentials.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public string? UserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The password for the user name associated with the credentials.
|
||||
/// The password for the username associated with the credentials.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public string? Password { get; set; }
|
||||
|
||||
@@ -329,4 +329,10 @@ public class WireMockServerSettings
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public string? AdminPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Defines the additional Handlebars Settings.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public HandlebarsSettings? HandlebarsSettings { get; set; }
|
||||
}
|
||||
@@ -84,6 +84,7 @@ public static class WireMockServerSettingsParser
|
||||
ParsePortSettings(settings, parser);
|
||||
ParseProxyAndRecordSettings(settings, parser);
|
||||
ParseCertificateSettings(settings, parser);
|
||||
ParseHandlebarsSettings(settings, parser);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -152,7 +153,7 @@ public static class WireMockServerSettingsParser
|
||||
}
|
||||
else if (settings.HostingScheme is null)
|
||||
{
|
||||
settings.Urls = parser.GetValues("Urls", new[] { "http://*:9091/" });
|
||||
settings.Urls = parser.GetValues("Urls", ["http://*:9091/"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,12 +167,25 @@ public static class WireMockServerSettingsParser
|
||||
X509CertificateFilePath = parser.GetStringValue("X509CertificateFilePath"),
|
||||
X509CertificatePassword = parser.GetStringValue("X509CertificatePassword")
|
||||
};
|
||||
|
||||
if (certificateSettings.IsDefined)
|
||||
{
|
||||
settings.CertificateSettings = certificateSettings;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParseHandlebarsSettings(WireMockServerSettings settings, SimpleSettingsParser parser)
|
||||
{
|
||||
if (parser.ContainsAny(nameof(HandlebarsSettings.AllowedCustomHandlebarsHelpers), nameof(HandlebarsSettings.AllowedHandlebarsHelpers)))
|
||||
{
|
||||
settings.HandlebarsSettings = new HandlebarsSettings
|
||||
{
|
||||
AllowedCustomHandlebarsHelpers = parser.GetEnumValue(nameof(HandlebarsSettings.AllowedCustomHandlebarsHelpers), CustomHandlebarsHelpers.None),
|
||||
AllowedHandlebarsHelpers = parser.GetEnumValues(nameof(HandlebarsSettings.AllowedHandlebarsHelpers), HandlebarsSettings.DefaultAllowedHandlebarsHelpers)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParseWebProxyAddressSettings(ProxyAndRecordSettings settings, SimpleSettingsParser parser)
|
||||
{
|
||||
string? proxyAddress = parser.GetStringValue("WebProxyAddress");
|
||||
|
||||
@@ -4,26 +4,30 @@ using HandlebarsDotNet;
|
||||
using HandlebarsDotNet.Helpers.Attributes;
|
||||
using HandlebarsDotNet.Helpers.Enums;
|
||||
using HandlebarsDotNet.Helpers.Helpers;
|
||||
using HandlebarsDotNet.Helpers.Options;
|
||||
using Stef.Validation;
|
||||
using WireMock.Handlers;
|
||||
using WireMock.Settings;
|
||||
|
||||
namespace WireMock.Transformers.Handlebars;
|
||||
|
||||
internal class FileHelpers : BaseHelpers, IHelpers
|
||||
{
|
||||
internal const string Name = "File";
|
||||
|
||||
private readonly IFileSystemHandler _fileSystemHandler;
|
||||
|
||||
public FileHelpers(IHandlebars context, IFileSystemHandler fileSystemHandler) : base(context)
|
||||
public FileHelpers(IHandlebars context, WireMockServerSettings settings) : base(context, new HandlebarsHelpersOptions())
|
||||
{
|
||||
_fileSystemHandler = Guard.NotNull(fileSystemHandler);
|
||||
_fileSystemHandler = Guard.NotNull(settings.FileSystemHandler);
|
||||
}
|
||||
|
||||
[HandlebarsWriter(WriterType.String, usage: HelperUsage.Both, passContext: true, name: "File")]
|
||||
[HandlebarsWriter(WriterType.String, usage: HelperUsage.Both, passContext: true, name: Name)]
|
||||
public string Read(Context context, string path)
|
||||
{
|
||||
var templateFunc = Context.Compile(path);
|
||||
var transformed = templateFunc(context.Value);
|
||||
return _fileSystemHandler.ReadResponseBodyAsString(transformed);
|
||||
var transformedPath = templateFunc(context.Value);
|
||||
return _fileSystemHandler.ReadResponseBodyAsString(transformedPath);
|
||||
}
|
||||
|
||||
public Category Category => Category.Custom;
|
||||
|
||||
@@ -23,7 +23,7 @@ internal class HandlebarsContextFactory : ITransformerContextFactory
|
||||
};
|
||||
var handlebars = HandlebarsDotNet.Handlebars.Create(config);
|
||||
|
||||
WireMockHandlebarsHelpers.Register(handlebars, _settings.FileSystemHandler);
|
||||
WireMockHandlebarsHelpers.Register(handlebars, _settings);
|
||||
|
||||
_settings.HandlebarsRegistrationCallback?.Invoke(handlebars, _settings.FileSystemHandler);
|
||||
|
||||
|
||||
@@ -2,21 +2,20 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using HandlebarsDotNet;
|
||||
using HandlebarsDotNet.Helpers;
|
||||
using HandlebarsDotNet.Helpers.Helpers;
|
||||
using WireMock.Handlers;
|
||||
using WireMock.Settings;
|
||||
using WireMock.Types;
|
||||
|
||||
namespace WireMock.Transformers.Handlebars;
|
||||
|
||||
internal static class WireMockHandlebarsHelpers
|
||||
{
|
||||
public static void Register(IHandlebars handlebarsContext, IFileSystemHandler fileSystemHandler)
|
||||
internal static void Register(IHandlebars handlebarsContext, WireMockServerSettings settings)
|
||||
{
|
||||
// Register https://github.com/StefH/Handlebars.Net.Helpers
|
||||
// Register https://github.com/Handlebars.Net/Handlebars.Net.Helpers
|
||||
HandlebarsHelpers.Register(handlebarsContext, o =>
|
||||
{
|
||||
var paths = new List<string>
|
||||
@@ -33,17 +32,18 @@ internal static class WireMockHandlebarsHelpers
|
||||
customHelperPaths.Add(path!);
|
||||
}
|
||||
}
|
||||
Add(Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location), paths);
|
||||
Add(Path.GetDirectoryName(Assembly.GetCallingAssembly().Location), paths);
|
||||
Add(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), paths);
|
||||
Add(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule?.FileName), paths);
|
||||
Add(Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly()?.Location), paths);
|
||||
Add(Path.GetDirectoryName(System.Reflection.Assembly.GetCallingAssembly().Location), paths);
|
||||
Add(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), paths);
|
||||
Add(Path.GetDirectoryName(System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName), paths);
|
||||
#endif
|
||||
o.CustomHelperPaths = paths;
|
||||
|
||||
o.CustomHelpers = new Dictionary<string, IHelpers>
|
||||
o.CustomHelpers = new Dictionary<string, IHelpers>();
|
||||
if (settings.HandlebarsSettings?.AllowedCustomHandlebarsHelpers.HasFlag(CustomHandlebarsHelpers.File) == true)
|
||||
{
|
||||
{ "File", new FileHelpers(handlebarsContext, fileSystemHandler) }
|
||||
};
|
||||
o.CustomHelpers.Add(FileHelpers.Name, new FileHelpers(handlebarsContext, settings));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
86
src/WireMock.Net/Util/FilePathUtils.cs
Normal file
86
src/WireMock.Net/Util/FilePathUtils.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.IO;
|
||||
using Stef.Validation;
|
||||
|
||||
namespace WireMock.Util;
|
||||
|
||||
internal static class FilePathUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Robust handling of the user defined path.
|
||||
/// Also supports Unix and Windows platforms
|
||||
/// </summary>
|
||||
/// <param name="path">The path to clean</param>
|
||||
public static string? CleanPath(string? path)
|
||||
{
|
||||
return path?.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes leading directory separator chars from the filepath, which could break Path.Combine
|
||||
/// </summary>
|
||||
/// <param name="path">The path to remove the loading DirectorySeparatorChars</param>
|
||||
public static string? RemoveLeadingDirectorySeparators(string? path)
|
||||
{
|
||||
return path?.TrimStart(Path.DirectorySeparatorChar);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combine two paths
|
||||
/// </summary>
|
||||
/// <param name="root">The root path</param>
|
||||
/// <param name="path">The path</param>
|
||||
public static string Combine(string root, string? path)
|
||||
{
|
||||
Guard.NotNull(root);
|
||||
|
||||
var result = RemoveLeadingDirectorySeparators(path);
|
||||
return result == null ? root : Path.Combine(root, result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a relative path from one path to another.
|
||||
/// </summary>
|
||||
/// <param name="relativeTo">The source path the result should be relative to. This path is always considered to be a directory..</param>
|
||||
/// <param name="path">The destination path.</param>
|
||||
/// <returns>The relative path, or path if the paths don't share the same root.</returns>
|
||||
public static string GetRelativePath(string relativeTo, string path)
|
||||
{
|
||||
#if NETCOREAPP3_1 || NET5_0_OR_GREATER || NETSTANDARD2_1
|
||||
return Path.GetRelativePath(relativeTo, path);
|
||||
#else
|
||||
Guard.NotNull(relativeTo);
|
||||
Guard.NotNull(path);
|
||||
|
||||
static string AppendDirectorySeparatorChar(string path)
|
||||
{
|
||||
// Append a slash only if the path is a directory and does not have a slash.
|
||||
if (!Path.HasExtension(path) && !path.EndsWith(Path.DirectorySeparatorChar.ToString()))
|
||||
{
|
||||
return path + Path.DirectorySeparatorChar;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
var fromUri = new System.Uri(AppendDirectorySeparatorChar(relativeTo));
|
||||
var toUri = new System.Uri(AppendDirectorySeparatorChar(path));
|
||||
|
||||
if (fromUri.Scheme != toUri.Scheme)
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
var relativeUri = fromUri.MakeRelativeUri(toUri);
|
||||
var relativePath = System.Uri.UnescapeDataString(relativeUri.ToString());
|
||||
|
||||
if (string.Equals(toUri.Scheme, "FILE", System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
|
||||
}
|
||||
|
||||
return relativePath;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.IO;
|
||||
using Stef.Validation;
|
||||
|
||||
namespace WireMock.Util;
|
||||
|
||||
internal static class PathUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Robust handling of the user defined path.
|
||||
/// Also supports Unix and Windows platforms
|
||||
/// </summary>
|
||||
/// <param name="path">The path to clean</param>
|
||||
public static string? CleanPath(string? path)
|
||||
{
|
||||
return path?.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes leading directory separator chars from the filepath, which could break Path.Combine
|
||||
/// </summary>
|
||||
/// <param name="path">The path to remove the loading DirectorySeparatorChars</param>
|
||||
public static string? RemoveLeadingDirectorySeparators(string? path)
|
||||
{
|
||||
return path?.TrimStart(new[] { Path.DirectorySeparatorChar });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combine two paths
|
||||
/// </summary>
|
||||
/// <param name="root">The root path</param>
|
||||
/// <param name="path">The path</param>
|
||||
public static string Combine(string root, string? path)
|
||||
{
|
||||
Guard.NotNull(root);
|
||||
|
||||
var result = RemoveLeadingDirectorySeparators(path);
|
||||
return result == null ? root : Path.Combine(root, result);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,85 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
#if PROTOBUF
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ProtoBufJsonConverter;
|
||||
using ProtoBufJsonConverter.Models;
|
||||
using Stef.Validation;
|
||||
using WireMock.Models;
|
||||
using WireMock.Settings;
|
||||
|
||||
namespace WireMock.Util;
|
||||
|
||||
internal static class ProtoDefinitionHelper
|
||||
/// <summary>
|
||||
/// Some helper methods for Proto Definitions.
|
||||
/// </summary>
|
||||
public static class ProtoDefinitionHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds a dictionary of ProtoDefinitions from a directory.
|
||||
/// - The key will be the filename without extension.
|
||||
/// - The value will be the ProtoDefinition with an extra comment with the relative path to each <c>.proto</c> file so it can be used by the WireMockProtoFileResolver.
|
||||
/// </summary>
|
||||
/// <param name="directory">The directory to start from.</param>
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <c>System.Threading.CancellationToken.None</c>.</param>
|
||||
public static async Task<ProtoDefinitionData> FromDirectory(string directory, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Guard.NotNullOrEmpty(directory);
|
||||
|
||||
var fileNameMappedToProtoDefinition = new Dictionary<string, string>();
|
||||
var filePaths = Directory.EnumerateFiles(directory, "*.proto", SearchOption.AllDirectories);
|
||||
|
||||
foreach (var filePath in filePaths)
|
||||
{
|
||||
// Get the relative path to the directory (note that this will be OS specific).
|
||||
var relativePath = FilePathUtils.GetRelativePath(directory, filePath);
|
||||
|
||||
// Make it a valid proto import path
|
||||
var protoRelativePath = relativePath.Replace(Path.DirectorySeparatorChar, '/');
|
||||
|
||||
// Build comment and get content from file.
|
||||
var comment = $"// {protoRelativePath}";
|
||||
#if NETSTANDARD2_0
|
||||
var content = File.ReadAllText(filePath);
|
||||
#else
|
||||
var content = await File.ReadAllTextAsync(filePath, cancellationToken);
|
||||
#endif
|
||||
// Only add the comment if it's not already defined.
|
||||
var modifiedContent = !content.StartsWith(comment) ? $"{comment}\n{content}" : content;
|
||||
var key = Path.GetFileNameWithoutExtension(filePath);
|
||||
|
||||
fileNameMappedToProtoDefinition.Add(key, modifiedContent);
|
||||
}
|
||||
|
||||
var converter = SingletonFactory<Converter>.GetInstance();
|
||||
var resolver = new WireMockProtoFileResolver(fileNameMappedToProtoDefinition.Values);
|
||||
|
||||
var messageTypeMappedToWithProtoDefinition = new Dictionary<string, string>();
|
||||
|
||||
foreach (var protoDefinition in fileNameMappedToProtoDefinition.Values)
|
||||
{
|
||||
var infoRequest = new GetInformationRequest(protoDefinition, resolver);
|
||||
|
||||
try
|
||||
{
|
||||
var info = await converter.GetInformationAsync(infoRequest, cancellationToken);
|
||||
foreach (var messageType in info.MessageTypes)
|
||||
{
|
||||
messageTypeMappedToWithProtoDefinition[messageType.Key] = protoDefinition;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
return new ProtoDefinitionData(fileNameMappedToProtoDefinition);
|
||||
}
|
||||
|
||||
internal static IdOrTexts GetIdOrTexts(WireMockServerSettings settings, params string[] protoDefinitionOrId)
|
||||
{
|
||||
switch (protoDefinitionOrId.Length)
|
||||
@@ -19,9 +92,10 @@ internal static class ProtoDefinitionHelper
|
||||
}
|
||||
|
||||
return new(null, protoDefinitionOrId);
|
||||
|
||||
|
||||
default:
|
||||
return new(null, protoDefinitionOrId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -7,18 +7,19 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using ProtoBufJsonConverter;
|
||||
using Stef.Validation;
|
||||
using WireMock.Extensions;
|
||||
|
||||
namespace WireMock.Util;
|
||||
|
||||
/// <summary>
|
||||
/// This resolver is used to resolve the extra ProtoDefinition files.
|
||||
///
|
||||
/// It assumes that:
|
||||
/// - the first ProtoDefinition file is the main ProtoDefinition file.
|
||||
/// - the first commented line of each extra ProtoDefinition file is the filename which is used in the import of the other ProtoDefinition file(s).
|
||||
/// - The first commented line of each ProtoDefinition file is the filepath which is used in the import of the other ProtoDefinition file(s).
|
||||
/// </summary>
|
||||
internal class WireMockProtoFileResolver : IProtoFileResolver
|
||||
{
|
||||
private readonly Dictionary<string, string> _files = new();
|
||||
private readonly Dictionary<string, string> _files = [];
|
||||
|
||||
public WireMockProtoFileResolver(IReadOnlyCollection<string> protoDefinitions)
|
||||
{
|
||||
@@ -27,12 +28,19 @@ internal class WireMockProtoFileResolver : IProtoFileResolver
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var extraProtoDefinition in protoDefinitions.Skip(1))
|
||||
foreach (var extraProtoDefinition in protoDefinitions)
|
||||
{
|
||||
var firstNonEmptyLine = extraProtoDefinition.Split(['\r', '\n']).FirstOrDefault(l => !string.IsNullOrEmpty(l));
|
||||
if (firstNonEmptyLine != null && TryGetValidFileName(firstNonEmptyLine.TrimStart(['/', ' ']), out var validFileName))
|
||||
if (firstNonEmptyLine != null)
|
||||
{
|
||||
_files.Add(validFileName, extraProtoDefinition);
|
||||
if (TryGetValidPath(firstNonEmptyLine.TrimStart(['/', ' ']), out var validPath))
|
||||
{
|
||||
_files.Add(validPath, extraProtoDefinition);
|
||||
}
|
||||
else
|
||||
{
|
||||
_files.Add(extraProtoDefinition.GetDeterministicHashCodeAsString(), extraProtoDefinition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,15 +60,15 @@ internal class WireMockProtoFileResolver : IProtoFileResolver
|
||||
throw new FileNotFoundException($"The ProtoDefinition '{path}' was not found.");
|
||||
}
|
||||
|
||||
private static bool TryGetValidFileName(string fileName, [NotNullWhen(true)] out string? validFileName)
|
||||
private static bool TryGetValidPath(string path, [NotNullWhen(true)] out string? validPath)
|
||||
{
|
||||
if (!fileName.Any(c => Path.GetInvalidFileNameChars().Contains(c)))
|
||||
if (!path.Any(c => Path.GetInvalidPathChars().Contains(c)))
|
||||
{
|
||||
validFileName = fileName;
|
||||
validPath = path;
|
||||
return true;
|
||||
}
|
||||
|
||||
validFileName = null;
|
||||
validPath = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
<DefineConstants>$(DefineConstants);USE_ASPNETCORE;NET46</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(TargetFramework)' != 'netstandard1.3' and '$(TargetFramework)' != 'net451' and '$(TargetFramework)' != 'net452'">
|
||||
<PropertyGroup Condition="'$(TargetFramework)' != 'netstandard1.3' and '$(TargetFramework)' != 'net451' and '$(TargetFramework)' != 'net452' and '$(TargetFramework)' != 'net46' and '$(TargetFramework)' != 'net461'">
|
||||
<DefineConstants>$(DefineConstants);OPENAPIPARSER</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -182,30 +182,30 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Handlebars.Net.Helpers" Version="2.4.10" />
|
||||
<!--<PackageReference Include="Handlebars.Net.Helpers.DynamicLinq" Version="2.4.10" />-->
|
||||
<PackageReference Include="Handlebars.Net.Helpers.Humanizer" Version="2.4.10" />
|
||||
<PackageReference Include="Handlebars.Net.Helpers.Json" Version="2.4.10" />
|
||||
<PackageReference Include="Handlebars.Net.Helpers.Random" Version="2.4.10" />
|
||||
<PackageReference Include="Handlebars.Net.Helpers.Xeger" Version="2.4.10" />
|
||||
<PackageReference Include="Handlebars.Net.Helpers.XPath" Version="2.4.10" />
|
||||
<PackageReference Include="Handlebars.Net.Helpers" Version="2.5.0" />
|
||||
<!--<PackageReference Include="Handlebars.Net.Helpers.DynamicLinq" Version="2.5.0" />-->
|
||||
<PackageReference Include="Handlebars.Net.Helpers.Humanizer" Version="2.5.0" />
|
||||
<PackageReference Include="Handlebars.Net.Helpers.Json" Version="2.5.0" />
|
||||
<PackageReference Include="Handlebars.Net.Helpers.Random" Version="2.5.0" />
|
||||
<PackageReference Include="Handlebars.Net.Helpers.Xeger" Version="2.5.0" />
|
||||
<PackageReference Include="Handlebars.Net.Helpers.XPath" Version="2.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' != 'netstandard1.3' and '$(TargetFramework)' != 'net451' and '$(TargetFramework)' != 'net452' ">
|
||||
<PackageReference Include="Handlebars.Net.Helpers.Xslt" Version="2.4.6" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- CVE-2021-26701 and https://github.com/WireMock-Net/WireMock.Net/issues/697 -->
|
||||
<!--<ItemGroup>
|
||||
--><!-- CVE-2021-26701 and https://github.com/WireMock-Net/WireMock.Net/issues/697 --><!--
|
||||
<PackageReference Include="System.Text.Encodings.Web" Version="4.7.2" />
|
||||
</ItemGroup>
|
||||
</ItemGroup>-->
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\WireMock.Net.Abstractions\WireMock.Net.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\WireMock.Org.Abstractions\WireMock.Org.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' != 'netstandard1.3' and '$(TargetFramework)' != 'net451' and '$(TargetFramework)' != 'net452'">
|
||||
<ItemGroup Condition="'$(TargetFramework)' != 'netstandard1.3' and '$(TargetFramework)' != 'net451' and '$(TargetFramework)' != 'net452' and '$(TargetFramework)' != 'net46' and '$(TargetFramework)' != 'net461'">
|
||||
<ProjectReference Include="..\WireMock.Net.OpenApiParser\WireMock.Net.OpenApiParser.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,30 +1,31 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsAspireHost>true</IsAspireHost>
|
||||
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>../../src/WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
|
||||
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- https://learn.microsoft.com/en-us/dotnet/aspire/extensibility/custom-resources?tabs=windows#create-library-for-resource-extension -->
|
||||
<ProjectReference Include="..\..\src\WireMock.Net.Aspire\WireMock.Net.Aspire.csproj" IsAspireProjectResource="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Hosting.AppHost" Version="8.2.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="WireMockMappings\*.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Sdk Name="Aspire.AppHost.Sdk" Version="9.2.0" />
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>../../src/WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
|
||||
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- https://learn.microsoft.com/en-us/dotnet/aspire/extensibility/custom-resources?tabs=windows#create-library-for-resource-extension -->
|
||||
<ProjectReference Include="..\..\src\WireMock.Net.Aspire\WireMock.Net.Aspire.csproj" IsAspireProjectResource="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="WireMockMappings\*.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,46 +1,48 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<SonarQubeExclude>true</SonarQubeExclude>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>../../src/WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
|
||||
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Hosting.Testing" Version="8.2.2" />
|
||||
<PackageReference Include="Codecov" Version="1.13.0" />
|
||||
<PackageReference Include="coverlet.msbuild" Version="6.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
<PackageReference Include="xunit" Version="2.8.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\WireMock.Net.Aspire\WireMock.Net.Aspire.csproj" IsAspireProjectResource="false" />
|
||||
<ProjectReference Include="..\WireMock.Net.Aspire.TestAppHost\WireMock.Net.Aspire.TestAppHost.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Aspire.Hosting.Testing" />
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<SonarQubeExclude>true</SonarQubeExclude>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>../../src/WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
|
||||
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Hosting.Testing" Version="9.2.0" />
|
||||
<PackageReference Include="Codecov" Version="1.13.0" />
|
||||
<PackageReference Include="coverlet.msbuild" Version="6.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
<PackageReference Include="xunit" Version="2.8.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\WireMock.Net.Aspire\WireMock.Net.Aspire.csproj" IsAspireProjectResource="false" />
|
||||
<ProjectReference Include="..\WireMock.Net.Aspire.TestAppHost\WireMock.Net.Aspire.TestAppHost.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Aspire.Hosting" />
|
||||
<Using Include="Aspire.Hosting.ApplicationModel" />
|
||||
<Using Include="Aspire.Hosting.Testing" />
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,96 +1,98 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Net.Sockets;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
|
||||
namespace WireMock.Net.Aspire.Tests;
|
||||
|
||||
public class WireMockServerBuilderExtensionsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData("\t")]
|
||||
public void AddWireMock_WithNullOrWhiteSpaceName_ShouldThrowException(string? name)
|
||||
{
|
||||
// Arrange
|
||||
var builder = Mock.Of<IDistributedApplicationBuilder>();
|
||||
|
||||
// Act
|
||||
Action act = () => builder.AddWireMock(name!, 12345);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<Exception>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddWireMock_WithInvalidPort_ShouldThrowArgumentOutOfRangeException()
|
||||
{
|
||||
// Arrange
|
||||
const int invalidPort = -1;
|
||||
var builder = Mock.Of<IDistributedApplicationBuilder>();
|
||||
|
||||
// Act
|
||||
Action act = () => builder.AddWireMock("ValidName", invalidPort);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<ArgumentOutOfRangeException>().WithMessage("Specified argument was out of the range of valid values. (Parameter 'port')");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddWireMock()
|
||||
{
|
||||
// Arrange
|
||||
var name = $"apiservice{Guid.NewGuid()}";
|
||||
const int port = 12345;
|
||||
const string username = "admin";
|
||||
const string password = "test";
|
||||
var builder = DistributedApplication.CreateBuilder();
|
||||
|
||||
// Act
|
||||
var wiremock = builder
|
||||
.AddWireMock(name, port)
|
||||
.WithAdminUserNameAndPassword(username, password)
|
||||
.WithReadStaticMappings();
|
||||
|
||||
// Assert
|
||||
wiremock.Resource.Should().NotBeNull();
|
||||
wiremock.Resource.Name.Should().Be(name);
|
||||
wiremock.Resource.Arguments.Should().BeEquivalentTo(new WireMockServerArguments
|
||||
{
|
||||
AdminPassword = password,
|
||||
AdminUsername = username,
|
||||
ReadStaticMappings = true,
|
||||
WatchStaticMappings = false,
|
||||
MappingsPath = null,
|
||||
HttpPort = port
|
||||
});
|
||||
wiremock.Resource.Annotations.Should().HaveCount(4);
|
||||
|
||||
var containerImageAnnotation = wiremock.Resource.Annotations.OfType<ContainerImageAnnotation>().FirstOrDefault();
|
||||
containerImageAnnotation.Should().BeEquivalentTo(new ContainerImageAnnotation
|
||||
{
|
||||
Image = "sheyenrath/wiremock.net-alpine",
|
||||
Registry = null,
|
||||
Tag = "latest"
|
||||
});
|
||||
|
||||
var endpointAnnotation = wiremock.Resource.Annotations.OfType<EndpointAnnotation>().FirstOrDefault();
|
||||
endpointAnnotation.Should().BeEquivalentTo(new EndpointAnnotation(
|
||||
protocol: ProtocolType.Tcp,
|
||||
uriScheme: "http",
|
||||
transport: null,
|
||||
name: null,
|
||||
port: port,
|
||||
targetPort: 80,
|
||||
isExternal: null,
|
||||
isProxied: true
|
||||
));
|
||||
|
||||
wiremock.Resource.Annotations.OfType<EnvironmentCallbackAnnotation>().FirstOrDefault().Should().NotBeNull();
|
||||
|
||||
wiremock.Resource.Annotations.OfType<CommandLineArgsCallbackAnnotation>().FirstOrDefault().Should().NotBeNull();
|
||||
}
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Net.Sockets;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
|
||||
namespace WireMock.Net.Aspire.Tests;
|
||||
|
||||
public class WireMockServerBuilderExtensionsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData("\t")]
|
||||
public void AddWireMock_WithNullOrWhiteSpaceName_ShouldThrowException(string? name)
|
||||
{
|
||||
// Arrange
|
||||
var builder = Mock.Of<IDistributedApplicationBuilder>();
|
||||
|
||||
// Act
|
||||
Action act = () => builder.AddWireMock(name!, 12345);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<Exception>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddWireMock_WithInvalidPort_ShouldThrowArgumentOutOfRangeException()
|
||||
{
|
||||
// Arrange
|
||||
const int invalidPort = -1;
|
||||
var builder = Mock.Of<IDistributedApplicationBuilder>();
|
||||
|
||||
// Act
|
||||
Action act = () => builder.AddWireMock("ValidName", invalidPort);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<ArgumentOutOfRangeException>().WithMessage("Specified argument was out of the range of valid values. (Parameter 'port')");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddWireMock()
|
||||
{
|
||||
// Arrange
|
||||
var name = $"apiservice{Guid.NewGuid()}";
|
||||
const int port = 12345;
|
||||
const string username = "admin";
|
||||
const string password = "test";
|
||||
var builder = DistributedApplication.CreateBuilder();
|
||||
|
||||
// Act
|
||||
var wiremock = builder
|
||||
.AddWireMock(name, port)
|
||||
.WithAdminUserNameAndPassword(username, password)
|
||||
.WithReadStaticMappings();
|
||||
|
||||
// Assert
|
||||
wiremock.Resource.Should().NotBeNull();
|
||||
wiremock.Resource.Name.Should().Be(name);
|
||||
wiremock.Resource.Arguments.Should().BeEquivalentTo(new WireMockServerArguments
|
||||
{
|
||||
AdminPassword = password,
|
||||
AdminUsername = username,
|
||||
ReadStaticMappings = true,
|
||||
WatchStaticMappings = false,
|
||||
MappingsPath = null,
|
||||
HttpPort = port
|
||||
});
|
||||
wiremock.Resource.Annotations.Should().HaveCount(5);
|
||||
|
||||
var containerImageAnnotation = wiremock.Resource.Annotations.OfType<ContainerImageAnnotation>().FirstOrDefault();
|
||||
containerImageAnnotation.Should().BeEquivalentTo(new ContainerImageAnnotation
|
||||
{
|
||||
Image = "sheyenrath/wiremock.net-alpine",
|
||||
Registry = null,
|
||||
Tag = "latest"
|
||||
});
|
||||
|
||||
var endpointAnnotation = wiremock.Resource.Annotations.OfType<EndpointAnnotation>().FirstOrDefault();
|
||||
endpointAnnotation.Should().BeEquivalentTo(new EndpointAnnotation(
|
||||
protocol: ProtocolType.Tcp,
|
||||
uriScheme: "http",
|
||||
transport: null,
|
||||
name: null,
|
||||
port: port,
|
||||
targetPort: 80,
|
||||
isExternal: null,
|
||||
isProxied: true
|
||||
));
|
||||
|
||||
wiremock.Resource.Annotations.OfType<EnvironmentCallbackAnnotation>().FirstOrDefault().Should().NotBeNull();
|
||||
|
||||
wiremock.Resource.Annotations.OfType<CommandLineArgsCallbackAnnotation>().FirstOrDefault().Should().NotBeNull();
|
||||
|
||||
wiremock.Resource.Annotations.OfType<ResourceCommandAnnotation>().FirstOrDefault().Should().NotBeNull();
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ public class IntegrationTests
|
||||
[Theory]
|
||||
[InlineData("/real1", "Hello 1 from WireMock.Net !")]
|
||||
[InlineData("/real2", "Hello 2 from WireMock.Net !")]
|
||||
[InlineData("/real3", "Hello 3 from WireMock.Net !")]
|
||||
public async Task CallingRealApi_WithAlwaysRedirectToWireMockIsTrue(string requestUri, string expectedResponse)
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -13,6 +13,9 @@ public class Program
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddSingleton<TaskQueue>();
|
||||
builder.Services.AddHostedService<TestBackgroundService>();
|
||||
|
||||
builder.Services.AddWireMockService(server =>
|
||||
{
|
||||
server.Given(Request.Create()
|
||||
@@ -28,6 +31,13 @@ public class Program
|
||||
).RespondWith(Response.Create()
|
||||
.WithBody("Hello 2 from WireMock.Net !")
|
||||
);
|
||||
|
||||
server.Given(Request.Create()
|
||||
.WithPath("/test3")
|
||||
.UsingAnyMethod()
|
||||
).RespondWith(Response.Create()
|
||||
.WithBody("Hello 3 from WireMock.Net !")
|
||||
);
|
||||
}, alwaysRedirectToWireMock);
|
||||
|
||||
var app = builder.Build();
|
||||
@@ -44,6 +54,11 @@ public class Program
|
||||
return await client.GetStringAsync("https://real-api:12345/test2");
|
||||
});
|
||||
|
||||
app.MapGet("/real3", async (TaskQueue taskQueue, CancellationToken cancellationToken) =>
|
||||
{
|
||||
return await taskQueue.Enqueue("https://real-api:12345/test3", cancellationToken);
|
||||
});
|
||||
|
||||
await app.RunAsync();
|
||||
}
|
||||
}
|
||||
38
test/WireMock.Net.TestWebApplication/TaskQueue.cs
Normal file
38
test/WireMock.Net.TestWebApplication/TaskQueue.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace WireMock.Net.TestWebApplication;
|
||||
|
||||
public class TaskQueue
|
||||
{
|
||||
private enum Status
|
||||
{
|
||||
Success,
|
||||
Error
|
||||
}
|
||||
|
||||
private readonly Channel<string> _taskChannel = Channel.CreateUnbounded<string>();
|
||||
private readonly Channel<(Status, string)> _responseChannel = Channel.CreateUnbounded<(Status, string)>();
|
||||
|
||||
public async Task<string> Enqueue(string taskId, CancellationToken cancellationToken)
|
||||
{
|
||||
await _taskChannel.Writer.WriteAsync(taskId, cancellationToken);
|
||||
var (status, result) = await _responseChannel.Reader.ReadAsync(cancellationToken);
|
||||
if (status == Status.Error)
|
||||
{
|
||||
throw new InvalidOperationException($"Received an error response from the task processor: ${result}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<string> ReadTasks(CancellationToken stoppingToken) =>
|
||||
_taskChannel.Reader.ReadAllAsync(stoppingToken);
|
||||
|
||||
public async Task WriteResponse(string result, CancellationToken stoppingToken) =>
|
||||
await _responseChannel.Writer.WriteAsync((Status.Success, result), stoppingToken);
|
||||
|
||||
public async Task WriteErrorResponse(string result, CancellationToken stoppingToken) =>
|
||||
await _responseChannel.Writer.WriteAsync((Status.Error, result), stoppingToken);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
namespace WireMock.Net.TestWebApplication;
|
||||
|
||||
public class TestBackgroundService(HttpClient client, TaskQueue taskQueue, ILogger<TestBackgroundService> logger)
|
||||
: BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
await foreach (var item in taskQueue.ReadTasks(stoppingToken))
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await client.GetStringAsync(item, stoppingToken);
|
||||
await taskQueue.WriteResponse(result, stoppingToken);
|
||||
}
|
||||
catch (ArgumentNullException argNullEx)
|
||||
{
|
||||
logger.LogError(argNullEx, "Null exception");
|
||||
await taskQueue.WriteErrorResponse(argNullEx.Message, stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
73
test/WireMock.Net.Tests/Grpc/ProtoDefinitionHelperTests.cs
Normal file
73
test/WireMock.Net.Tests/Grpc/ProtoDefinitionHelperTests.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
#if PROTOBUF
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using WireMock.Util;
|
||||
using Xunit;
|
||||
|
||||
namespace WireMock.Net.Tests.Grpc;
|
||||
|
||||
public class ProtoDefinitionHelperTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task FromDirectory_Greet_ShouldReturnModifiedProtoFiles()
|
||||
{
|
||||
// Arrange
|
||||
var directory = Path.Combine(Directory.GetCurrentDirectory(), "Grpc", "Test");
|
||||
var expectedFilename = "SubFolder/request.proto";
|
||||
var expectedComment = $"// {expectedFilename}";
|
||||
|
||||
// Act
|
||||
var protoDefinitionData = await ProtoDefinitionHelper.FromDirectory(directory);
|
||||
var protoDefinitions = protoDefinitionData.ToList("greet");
|
||||
|
||||
// Assert
|
||||
protoDefinitions.Should().HaveCount(2);
|
||||
protoDefinitions[0].Should().StartWith("// greet.proto");
|
||||
protoDefinitions[1].Should().StartWith(expectedComment);
|
||||
|
||||
// Arrange
|
||||
var resolver = new WireMockProtoFileResolver(protoDefinitions);
|
||||
|
||||
// Act + Assert
|
||||
resolver.Exists(expectedFilename).Should().BeTrue();
|
||||
resolver.Exists("x").Should().BeFalse();
|
||||
|
||||
// Act + Assert
|
||||
var text = await resolver.OpenText(expectedFilename).ReadToEndAsync();
|
||||
text.Should().StartWith(expectedComment);
|
||||
System.Action action = () => resolver.OpenText("x");
|
||||
action.Should().Throw<FileNotFoundException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FromDirectory_OpenTelemetry_ShouldReturnModifiedProtoFiles()
|
||||
{
|
||||
// Arrange
|
||||
var directory = Path.Combine(Directory.GetCurrentDirectory(), "Grpc", "ot");
|
||||
|
||||
// Act
|
||||
var protoDefinitionData = await ProtoDefinitionHelper.FromDirectory(directory);
|
||||
var protoDefinitions = protoDefinitionData.ToList("trace_service");
|
||||
|
||||
// Assert
|
||||
protoDefinitions.Should().HaveCount(10);
|
||||
|
||||
var responseBytes = await ProtoBufUtils.GetProtoBufMessageWithHeaderAsync(
|
||||
protoDefinitions,
|
||||
"OpenTelemetry.Proto.Collector.Trace.V1.ExportTracePartialSuccess",
|
||||
new
|
||||
{
|
||||
rejected_spans = 1,
|
||||
error_message = "abc"
|
||||
}
|
||||
);
|
||||
|
||||
// Assert
|
||||
Convert.ToBase64String(responseBytes).Should().Be("AAAAAAcIARIDYWJj");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package greet;
|
||||
|
||||
message HelloRequest {
|
||||
string name = 1;
|
||||
}
|
||||
13
test/WireMock.Net.Tests/Grpc/Test/greet.proto
Normal file
13
test/WireMock.Net.Tests/Grpc/Test/greet.proto
Normal file
@@ -0,0 +1,13 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "SubFolder/request.proto";
|
||||
|
||||
package greet;
|
||||
|
||||
service Greeter {
|
||||
rpc SayHello (HelloRequest) returns (HelloReply);
|
||||
}
|
||||
|
||||
message HelloReply {
|
||||
string message = 1;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# OpenTelemetry Collector Proto
|
||||
|
||||
This package describes the OpenTelemetry collector protocol.
|
||||
|
||||
## Packages
|
||||
|
||||
1. `common` package contains the common messages shared between different services.
|
||||
2. `trace` package contains the Trace Service protos.
|
||||
3. `metrics` package contains the Metrics Service protos.
|
||||
4. `logs` package contains the Logs Service protos.
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright 2020, OpenTelemetry Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package opentelemetry.proto.collector.logs.v1;
|
||||
|
||||
import "opentelemetry/proto/logs/v1/logs.proto";
|
||||
|
||||
option csharp_namespace = "OpenTelemetry.Proto.Collector.Logs.V1";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "io.opentelemetry.proto.collector.logs.v1";
|
||||
option java_outer_classname = "LogsServiceProto";
|
||||
option go_package = "go.opentelemetry.io/proto/otlp/collector/logs/v1";
|
||||
|
||||
// Service that can be used to push logs between one Application instrumented with
|
||||
// OpenTelemetry and an collector, or between an collector and a central collector (in this
|
||||
// case logs are sent/received to/from multiple Applications).
|
||||
service LogsService {
|
||||
// For performance reasons, it is recommended to keep this RPC
|
||||
// alive for the entire life of the application.
|
||||
rpc Export(ExportLogsServiceRequest) returns (ExportLogsServiceResponse) {}
|
||||
}
|
||||
|
||||
message ExportLogsServiceRequest {
|
||||
// An array of ResourceLogs.
|
||||
// For data coming from a single resource this array will typically contain one
|
||||
// element. Intermediary nodes (such as OpenTelemetry Collector) that receive
|
||||
// data from multiple origins typically batch the data before forwarding further and
|
||||
// in that case this array will contain multiple elements.
|
||||
repeated opentelemetry.proto.logs.v1.ResourceLogs resource_logs = 1;
|
||||
}
|
||||
|
||||
message ExportLogsServiceResponse {
|
||||
// The details of a partially successful export request.
|
||||
//
|
||||
// If the request is only partially accepted
|
||||
// (i.e. when the server accepts only parts of the data and rejects the rest)
|
||||
// the server MUST initialize the `partial_success` field and MUST
|
||||
// set the `rejected_<signal>` with the number of items it rejected.
|
||||
//
|
||||
// Servers MAY also make use of the `partial_success` field to convey
|
||||
// warnings/suggestions to senders even when the request was fully accepted.
|
||||
// In such cases, the `rejected_<signal>` MUST have a value of `0` and
|
||||
// the `error_message` MUST be non-empty.
|
||||
//
|
||||
// A `partial_success` message with an empty value (rejected_<signal> = 0 and
|
||||
// `error_message` = "") is equivalent to it not being set/present. Senders
|
||||
// SHOULD interpret it the same way as in the full success case.
|
||||
ExportLogsPartialSuccess partial_success = 1;
|
||||
}
|
||||
|
||||
message ExportLogsPartialSuccess {
|
||||
// The number of rejected log records.
|
||||
//
|
||||
// A `rejected_<signal>` field holding a `0` value indicates that the
|
||||
// request was fully accepted.
|
||||
int64 rejected_log_records = 1;
|
||||
|
||||
// A developer-facing human-readable message in English. It should be used
|
||||
// either to explain why the server rejected parts of the data during a partial
|
||||
// success or to convey warnings/suggestions during a full success. The message
|
||||
// should offer guidance on how users can address such issues.
|
||||
//
|
||||
// error_message is an optional field. An error_message with an empty value
|
||||
// is equivalent to it not being set.
|
||||
string error_message = 2;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# This is an API configuration to generate an HTTP/JSON -> gRPC gateway for the
|
||||
# OpenTelemetry service using github.com/grpc-ecosystem/grpc-gateway.
|
||||
type: google.api.Service
|
||||
config_version: 3
|
||||
http:
|
||||
rules:
|
||||
- selector: opentelemetry.proto.collector.logs.v1.LogsService.Export
|
||||
post: /v1/logs
|
||||
body: "*"
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright 2019, OpenTelemetry Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package opentelemetry.proto.collector.metrics.v1;
|
||||
|
||||
import "opentelemetry/proto/metrics/v1/metrics.proto";
|
||||
|
||||
option csharp_namespace = "OpenTelemetry.Proto.Collector.Metrics.V1";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "io.opentelemetry.proto.collector.metrics.v1";
|
||||
option java_outer_classname = "MetricsServiceProto";
|
||||
option go_package = "go.opentelemetry.io/proto/otlp/collector/metrics/v1";
|
||||
|
||||
// Service that can be used to push metrics between one Application
|
||||
// instrumented with OpenTelemetry and a collector, or between a collector and a
|
||||
// central collector.
|
||||
service MetricsService {
|
||||
// For performance reasons, it is recommended to keep this RPC
|
||||
// alive for the entire life of the application.
|
||||
rpc Export(ExportMetricsServiceRequest) returns (ExportMetricsServiceResponse) {}
|
||||
}
|
||||
|
||||
message ExportMetricsServiceRequest {
|
||||
// An array of ResourceMetrics.
|
||||
// For data coming from a single resource this array will typically contain one
|
||||
// element. Intermediary nodes (such as OpenTelemetry Collector) that receive
|
||||
// data from multiple origins typically batch the data before forwarding further and
|
||||
// in that case this array will contain multiple elements.
|
||||
repeated opentelemetry.proto.metrics.v1.ResourceMetrics resource_metrics = 1;
|
||||
}
|
||||
|
||||
message ExportMetricsServiceResponse {
|
||||
// The details of a partially successful export request.
|
||||
//
|
||||
// If the request is only partially accepted
|
||||
// (i.e. when the server accepts only parts of the data and rejects the rest)
|
||||
// the server MUST initialize the `partial_success` field and MUST
|
||||
// set the `rejected_<signal>` with the number of items it rejected.
|
||||
//
|
||||
// Servers MAY also make use of the `partial_success` field to convey
|
||||
// warnings/suggestions to senders even when the request was fully accepted.
|
||||
// In such cases, the `rejected_<signal>` MUST have a value of `0` and
|
||||
// the `error_message` MUST be non-empty.
|
||||
//
|
||||
// A `partial_success` message with an empty value (rejected_<signal> = 0 and
|
||||
// `error_message` = "") is equivalent to it not being set/present. Senders
|
||||
// SHOULD interpret it the same way as in the full success case.
|
||||
ExportMetricsPartialSuccess partial_success = 1;
|
||||
}
|
||||
|
||||
message ExportMetricsPartialSuccess {
|
||||
// The number of rejected data points.
|
||||
//
|
||||
// A `rejected_<signal>` field holding a `0` value indicates that the
|
||||
// request was fully accepted.
|
||||
int64 rejected_data_points = 1;
|
||||
|
||||
// A developer-facing human-readable message in English. It should be used
|
||||
// either to explain why the server rejected parts of the data during a partial
|
||||
// success or to convey warnings/suggestions during a full success. The message
|
||||
// should offer guidance on how users can address such issues.
|
||||
//
|
||||
// error_message is an optional field. An error_message with an empty value
|
||||
// is equivalent to it not being set.
|
||||
string error_message = 2;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# This is an API configuration to generate an HTTP/JSON -> gRPC gateway for the
|
||||
# OpenTelemetry service using github.com/grpc-ecosystem/grpc-gateway.
|
||||
type: google.api.Service
|
||||
config_version: 3
|
||||
http:
|
||||
rules:
|
||||
- selector: opentelemetry.proto.collector.metrics.v1.MetricsService.Export
|
||||
post: /v1/metrics
|
||||
body: "*"
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright 2023, OpenTelemetry Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package opentelemetry.proto.collector.profiles.v1development;
|
||||
|
||||
import "opentelemetry/proto/profiles/v1development/profiles.proto";
|
||||
|
||||
option csharp_namespace = "OpenTelemetry.Proto.Collector.Profiles.V1Development";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "io.opentelemetry.proto.collector.profiles.v1development";
|
||||
option java_outer_classname = "ProfilesServiceProto";
|
||||
option go_package = "go.opentelemetry.io/proto/otlp/collector/profiles/v1development";
|
||||
|
||||
// Service that can be used to push profiles between one Application instrumented with
|
||||
// OpenTelemetry and a collector, or between a collector and a central collector.
|
||||
service ProfilesService {
|
||||
// For performance reasons, it is recommended to keep this RPC
|
||||
// alive for the entire life of the application.
|
||||
rpc Export(ExportProfilesServiceRequest) returns (ExportProfilesServiceResponse) {}
|
||||
}
|
||||
|
||||
message ExportProfilesServiceRequest {
|
||||
// An array of ResourceProfiles.
|
||||
// For data coming from a single resource this array will typically contain one
|
||||
// element. Intermediary nodes (such as OpenTelemetry Collector) that receive
|
||||
// data from multiple origins typically batch the data before forwarding further and
|
||||
// in that case this array will contain multiple elements.
|
||||
repeated opentelemetry.proto.profiles.v1development.ResourceProfiles resource_profiles = 1;
|
||||
}
|
||||
|
||||
message ExportProfilesServiceResponse {
|
||||
// The details of a partially successful export request.
|
||||
//
|
||||
// If the request is only partially accepted
|
||||
// (i.e. when the server accepts only parts of the data and rejects the rest)
|
||||
// the server MUST initialize the `partial_success` field and MUST
|
||||
// set the `rejected_<signal>` with the number of items it rejected.
|
||||
//
|
||||
// Servers MAY also make use of the `partial_success` field to convey
|
||||
// warnings/suggestions to senders even when the request was fully accepted.
|
||||
// In such cases, the `rejected_<signal>` MUST have a value of `0` and
|
||||
// the `error_message` MUST be non-empty.
|
||||
//
|
||||
// A `partial_success` message with an empty value (rejected_<signal> = 0 and
|
||||
// `error_message` = "") is equivalent to it not being set/present. Senders
|
||||
// SHOULD interpret it the same way as in the full success case.
|
||||
ExportProfilesPartialSuccess partial_success = 1;
|
||||
}
|
||||
|
||||
message ExportProfilesPartialSuccess {
|
||||
// The number of rejected profiles.
|
||||
//
|
||||
// A `rejected_<signal>` field holding a `0` value indicates that the
|
||||
// request was fully accepted.
|
||||
int64 rejected_profiles = 1;
|
||||
|
||||
// A developer-facing human-readable message in English. It should be used
|
||||
// either to explain why the server rejected parts of the data during a partial
|
||||
// success or to convey warnings/suggestions during a full success. The message
|
||||
// should offer guidance on how users can address such issues.
|
||||
//
|
||||
// error_message is an optional field. An error_message with an empty value
|
||||
// is equivalent to it not being set.
|
||||
string error_message = 2;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# This is an API configuration to generate an HTTP/JSON -> gRPC gateway for the
|
||||
# OpenTelemetry service using github.com/grpc-ecosystem/grpc-gateway.
|
||||
type: google.api.Service
|
||||
config_version: 3
|
||||
http:
|
||||
rules:
|
||||
- selector: opentelemetry.proto.collector.profiles.v1development.ProfilesService.Export
|
||||
post: /v1development/profiles
|
||||
body: "*"
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright 2019, OpenTelemetry Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package opentelemetry.proto.collector.trace.v1;
|
||||
|
||||
import "opentelemetry/proto/trace/v1/trace.proto";
|
||||
|
||||
option csharp_namespace = "OpenTelemetry.Proto.Collector.Trace.V1";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "io.opentelemetry.proto.collector.trace.v1";
|
||||
option java_outer_classname = "TraceServiceProto";
|
||||
option go_package = "go.opentelemetry.io/proto/otlp/collector/trace/v1";
|
||||
|
||||
// Service that can be used to push spans between one Application instrumented with
|
||||
// OpenTelemetry and a collector, or between a collector and a central collector (in this
|
||||
// case spans are sent/received to/from multiple Applications).
|
||||
service TraceService {
|
||||
// For performance reasons, it is recommended to keep this RPC
|
||||
// alive for the entire life of the application.
|
||||
rpc Export(ExportTraceServiceRequest) returns (ExportTraceServiceResponse) {}
|
||||
}
|
||||
|
||||
message ExportTraceServiceRequest {
|
||||
// An array of ResourceSpans.
|
||||
// For data coming from a single resource this array will typically contain one
|
||||
// element. Intermediary nodes (such as OpenTelemetry Collector) that receive
|
||||
// data from multiple origins typically batch the data before forwarding further and
|
||||
// in that case this array will contain multiple elements.
|
||||
repeated opentelemetry.proto.trace.v1.ResourceSpans resource_spans = 1;
|
||||
}
|
||||
|
||||
message ExportTraceServiceResponse {
|
||||
// The details of a partially successful export request.
|
||||
//
|
||||
// If the request is only partially accepted
|
||||
// (i.e. when the server accepts only parts of the data and rejects the rest)
|
||||
// the server MUST initialize the `partial_success` field and MUST
|
||||
// set the `rejected_<signal>` with the number of items it rejected.
|
||||
//
|
||||
// Servers MAY also make use of the `partial_success` field to convey
|
||||
// warnings/suggestions to senders even when the request was fully accepted.
|
||||
// In such cases, the `rejected_<signal>` MUST have a value of `0` and
|
||||
// the `error_message` MUST be non-empty.
|
||||
//
|
||||
// A `partial_success` message with an empty value (rejected_<signal> = 0 and
|
||||
// `error_message` = "") is equivalent to it not being set/present. Senders
|
||||
// SHOULD interpret it the same way as in the full success case.
|
||||
ExportTracePartialSuccess partial_success = 1;
|
||||
}
|
||||
|
||||
message ExportTracePartialSuccess {
|
||||
// The number of rejected spans.
|
||||
//
|
||||
// A `rejected_<signal>` field holding a `0` value indicates that the
|
||||
// request was fully accepted.
|
||||
int64 rejected_spans = 1;
|
||||
|
||||
// A developer-facing human-readable message in English. It should be used
|
||||
// either to explain why the server rejected parts of the data during a partial
|
||||
// success or to convey warnings/suggestions during a full success. The message
|
||||
// should offer guidance on how users can address such issues.
|
||||
//
|
||||
// error_message is an optional field. An error_message with an empty value
|
||||
// is equivalent to it not being set.
|
||||
string error_message = 2;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# This is an API configuration to generate an HTTP/JSON -> gRPC gateway for the
|
||||
# OpenTelemetry service using github.com/grpc-ecosystem/grpc-gateway.
|
||||
type: google.api.Service
|
||||
config_version: 3
|
||||
http:
|
||||
rules:
|
||||
- selector: opentelemetry.proto.collector.trace.v1.TraceService.Export
|
||||
post: /v1/traces
|
||||
body: "*"
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2019, OpenTelemetry Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package opentelemetry.proto.common.v1;
|
||||
|
||||
option csharp_namespace = "OpenTelemetry.Proto.Common.V1";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "io.opentelemetry.proto.common.v1";
|
||||
option java_outer_classname = "CommonProto";
|
||||
option go_package = "go.opentelemetry.io/proto/otlp/common/v1";
|
||||
|
||||
// AnyValue is used to represent any type of attribute value. AnyValue may contain a
|
||||
// primitive value such as a string or integer or it may contain an arbitrary nested
|
||||
// object containing arrays, key-value lists and primitives.
|
||||
message AnyValue {
|
||||
// The value is one of the listed fields. It is valid for all values to be unspecified
|
||||
// in which case this AnyValue is considered to be "empty".
|
||||
oneof value {
|
||||
string string_value = 1;
|
||||
bool bool_value = 2;
|
||||
int64 int_value = 3;
|
||||
double double_value = 4;
|
||||
ArrayValue array_value = 5;
|
||||
KeyValueList kvlist_value = 6;
|
||||
bytes bytes_value = 7;
|
||||
}
|
||||
}
|
||||
|
||||
// ArrayValue is a list of AnyValue messages. We need ArrayValue as a message
|
||||
// since oneof in AnyValue does not allow repeated fields.
|
||||
message ArrayValue {
|
||||
// Array of values. The array may be empty (contain 0 elements).
|
||||
repeated AnyValue values = 1;
|
||||
}
|
||||
|
||||
// KeyValueList is a list of KeyValue messages. We need KeyValueList as a message
|
||||
// since `oneof` in AnyValue does not allow repeated fields. Everywhere else where we need
|
||||
// a list of KeyValue messages (e.g. in Span) we use `repeated KeyValue` directly to
|
||||
// avoid unnecessary extra wrapping (which slows down the protocol). The 2 approaches
|
||||
// are semantically equivalent.
|
||||
message KeyValueList {
|
||||
// A collection of key/value pairs of key-value pairs. The list may be empty (may
|
||||
// contain 0 elements).
|
||||
// The keys MUST be unique (it is not allowed to have more than one
|
||||
// value with the same key).
|
||||
repeated KeyValue values = 1;
|
||||
}
|
||||
|
||||
// KeyValue is a key-value pair that is used to store Span attributes, Link
|
||||
// attributes, etc.
|
||||
message KeyValue {
|
||||
string key = 1;
|
||||
AnyValue value = 2;
|
||||
}
|
||||
|
||||
// InstrumentationScope is a message representing the instrumentation scope information
|
||||
// such as the fully qualified name and version.
|
||||
message InstrumentationScope {
|
||||
// An empty instrumentation scope name means the name is unknown.
|
||||
string name = 1;
|
||||
string version = 2;
|
||||
|
||||
// Additional attributes that describe the scope. [Optional].
|
||||
// Attribute keys MUST be unique (it is not allowed to have more than one
|
||||
// attribute with the same key).
|
||||
repeated KeyValue attributes = 3;
|
||||
uint32 dropped_attributes_count = 4;
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
// Copyright 2020, OpenTelemetry Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package opentelemetry.proto.logs.v1;
|
||||
|
||||
import "opentelemetry/proto/common/v1/common.proto";
|
||||
import "opentelemetry/proto/resource/v1/resource.proto";
|
||||
|
||||
option csharp_namespace = "OpenTelemetry.Proto.Logs.V1";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "io.opentelemetry.proto.logs.v1";
|
||||
option java_outer_classname = "LogsProto";
|
||||
option go_package = "go.opentelemetry.io/proto/otlp/logs/v1";
|
||||
|
||||
// LogsData represents the logs data that can be stored in a persistent storage,
|
||||
// OR can be embedded by other protocols that transfer OTLP logs data but do not
|
||||
// implement the OTLP protocol.
|
||||
//
|
||||
// The main difference between this message and collector protocol is that
|
||||
// in this message there will not be any "control" or "metadata" specific to
|
||||
// OTLP protocol.
|
||||
//
|
||||
// When new fields are added into this message, the OTLP request MUST be updated
|
||||
// as well.
|
||||
message LogsData {
|
||||
// An array of ResourceLogs.
|
||||
// For data coming from a single resource this array will typically contain
|
||||
// one element. Intermediary nodes that receive data from multiple origins
|
||||
// typically batch the data before forwarding further and in that case this
|
||||
// array will contain multiple elements.
|
||||
repeated ResourceLogs resource_logs = 1;
|
||||
}
|
||||
|
||||
// A collection of ScopeLogs from a Resource.
|
||||
message ResourceLogs {
|
||||
reserved 1000;
|
||||
|
||||
// The resource for the logs in this message.
|
||||
// If this field is not set then resource info is unknown.
|
||||
opentelemetry.proto.resource.v1.Resource resource = 1;
|
||||
|
||||
// A list of ScopeLogs that originate from a resource.
|
||||
repeated ScopeLogs scope_logs = 2;
|
||||
|
||||
// The Schema URL, if known. This is the identifier of the Schema that the resource data
|
||||
// is recorded in. Notably, the last part of the URL path is the version number of the
|
||||
// schema: http[s]://server[:port]/path/<version>. To learn more about Schema URL see
|
||||
// https://opentelemetry.io/docs/specs/otel/schemas/#schema-url
|
||||
// This schema_url applies to the data in the "resource" field. It does not apply
|
||||
// to the data in the "scope_logs" field which have their own schema_url field.
|
||||
string schema_url = 3;
|
||||
}
|
||||
|
||||
// A collection of Logs produced by a Scope.
|
||||
message ScopeLogs {
|
||||
// The instrumentation scope information for the logs in this message.
|
||||
// Semantically when InstrumentationScope isn't set, it is equivalent with
|
||||
// an empty instrumentation scope name (unknown).
|
||||
opentelemetry.proto.common.v1.InstrumentationScope scope = 1;
|
||||
|
||||
// A list of log records.
|
||||
repeated LogRecord log_records = 2;
|
||||
|
||||
// The Schema URL, if known. This is the identifier of the Schema that the log data
|
||||
// is recorded in. Notably, the last part of the URL path is the version number of the
|
||||
// schema: http[s]://server[:port]/path/<version>. To learn more about Schema URL see
|
||||
// https://opentelemetry.io/docs/specs/otel/schemas/#schema-url
|
||||
// This schema_url applies to all logs in the "logs" field.
|
||||
string schema_url = 3;
|
||||
}
|
||||
|
||||
// Possible values for LogRecord.SeverityNumber.
|
||||
enum SeverityNumber {
|
||||
// UNSPECIFIED is the default SeverityNumber, it MUST NOT be used.
|
||||
SEVERITY_NUMBER_UNSPECIFIED = 0;
|
||||
SEVERITY_NUMBER_TRACE = 1;
|
||||
SEVERITY_NUMBER_TRACE2 = 2;
|
||||
SEVERITY_NUMBER_TRACE3 = 3;
|
||||
SEVERITY_NUMBER_TRACE4 = 4;
|
||||
SEVERITY_NUMBER_DEBUG = 5;
|
||||
SEVERITY_NUMBER_DEBUG2 = 6;
|
||||
SEVERITY_NUMBER_DEBUG3 = 7;
|
||||
SEVERITY_NUMBER_DEBUG4 = 8;
|
||||
SEVERITY_NUMBER_INFO = 9;
|
||||
SEVERITY_NUMBER_INFO2 = 10;
|
||||
SEVERITY_NUMBER_INFO3 = 11;
|
||||
SEVERITY_NUMBER_INFO4 = 12;
|
||||
SEVERITY_NUMBER_WARN = 13;
|
||||
SEVERITY_NUMBER_WARN2 = 14;
|
||||
SEVERITY_NUMBER_WARN3 = 15;
|
||||
SEVERITY_NUMBER_WARN4 = 16;
|
||||
SEVERITY_NUMBER_ERROR = 17;
|
||||
SEVERITY_NUMBER_ERROR2 = 18;
|
||||
SEVERITY_NUMBER_ERROR3 = 19;
|
||||
SEVERITY_NUMBER_ERROR4 = 20;
|
||||
SEVERITY_NUMBER_FATAL = 21;
|
||||
SEVERITY_NUMBER_FATAL2 = 22;
|
||||
SEVERITY_NUMBER_FATAL3 = 23;
|
||||
SEVERITY_NUMBER_FATAL4 = 24;
|
||||
}
|
||||
|
||||
// LogRecordFlags represents constants used to interpret the
|
||||
// LogRecord.flags field, which is protobuf 'fixed32' type and is to
|
||||
// be used as bit-fields. Each non-zero value defined in this enum is
|
||||
// a bit-mask. To extract the bit-field, for example, use an
|
||||
// expression like:
|
||||
//
|
||||
// (logRecord.flags & LOG_RECORD_FLAGS_TRACE_FLAGS_MASK)
|
||||
//
|
||||
enum LogRecordFlags {
|
||||
// The zero value for the enum. Should not be used for comparisons.
|
||||
// Instead use bitwise "and" with the appropriate mask as shown above.
|
||||
LOG_RECORD_FLAGS_DO_NOT_USE = 0;
|
||||
|
||||
// Bits 0-7 are used for trace flags.
|
||||
LOG_RECORD_FLAGS_TRACE_FLAGS_MASK = 0x000000FF;
|
||||
|
||||
// Bits 8-31 are reserved for future use.
|
||||
}
|
||||
|
||||
// A log record according to OpenTelemetry Log Data Model:
|
||||
// https://github.com/open-telemetry/oteps/blob/main/text/logs/0097-log-data-model.md
|
||||
message LogRecord {
|
||||
reserved 4;
|
||||
|
||||
// time_unix_nano is the time when the event occurred.
|
||||
// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970.
|
||||
// Value of 0 indicates unknown or missing timestamp.
|
||||
fixed64 time_unix_nano = 1;
|
||||
|
||||
// Time when the event was observed by the collection system.
|
||||
// For events that originate in OpenTelemetry (e.g. using OpenTelemetry Logging SDK)
|
||||
// this timestamp is typically set at the generation time and is equal to Timestamp.
|
||||
// For events originating externally and collected by OpenTelemetry (e.g. using
|
||||
// Collector) this is the time when OpenTelemetry's code observed the event measured
|
||||
// by the clock of the OpenTelemetry code. This field MUST be set once the event is
|
||||
// observed by OpenTelemetry.
|
||||
//
|
||||
// For converting OpenTelemetry log data to formats that support only one timestamp or
|
||||
// when receiving OpenTelemetry log data by recipients that support only one timestamp
|
||||
// internally the following logic is recommended:
|
||||
// - Use time_unix_nano if it is present, otherwise use observed_time_unix_nano.
|
||||
//
|
||||
// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970.
|
||||
// Value of 0 indicates unknown or missing timestamp.
|
||||
fixed64 observed_time_unix_nano = 11;
|
||||
|
||||
// Numerical value of the severity, normalized to values described in Log Data Model.
|
||||
// [Optional].
|
||||
SeverityNumber severity_number = 2;
|
||||
|
||||
// The severity text (also known as log level). The original string representation as
|
||||
// it is known at the source. [Optional].
|
||||
string severity_text = 3;
|
||||
|
||||
// A value containing the body of the log record. Can be for example a human-readable
|
||||
// string message (including multi-line) describing the event in a free form or it can
|
||||
// be a structured data composed of arrays and maps of other values. [Optional].
|
||||
opentelemetry.proto.common.v1.AnyValue body = 5;
|
||||
|
||||
// Additional attributes that describe the specific event occurrence. [Optional].
|
||||
// Attribute keys MUST be unique (it is not allowed to have more than one
|
||||
// attribute with the same key).
|
||||
repeated opentelemetry.proto.common.v1.KeyValue attributes = 6;
|
||||
uint32 dropped_attributes_count = 7;
|
||||
|
||||
// Flags, a bit field. 8 least significant bits are the trace flags as
|
||||
// defined in W3C Trace Context specification. 24 most significant bits are reserved
|
||||
// and must be set to 0. Readers must not assume that 24 most significant bits
|
||||
// will be zero and must correctly mask the bits when reading 8-bit trace flag (use
|
||||
// flags & LOG_RECORD_FLAGS_TRACE_FLAGS_MASK). [Optional].
|
||||
fixed32 flags = 8;
|
||||
|
||||
// A unique identifier for a trace. All logs from the same trace share
|
||||
// the same `trace_id`. The ID is a 16-byte array. An ID with all zeroes OR
|
||||
// of length other than 16 bytes is considered invalid (empty string in OTLP/JSON
|
||||
// is zero-length and thus is also invalid).
|
||||
//
|
||||
// This field is optional.
|
||||
//
|
||||
// The receivers SHOULD assume that the log record is not associated with a
|
||||
// trace if any of the following is true:
|
||||
// - the field is not present,
|
||||
// - the field contains an invalid value.
|
||||
bytes trace_id = 9;
|
||||
|
||||
// A unique identifier for a span within a trace, assigned when the span
|
||||
// is created. The ID is an 8-byte array. An ID with all zeroes OR of length
|
||||
// other than 8 bytes is considered invalid (empty string in OTLP/JSON
|
||||
// is zero-length and thus is also invalid).
|
||||
//
|
||||
// This field is optional. If the sender specifies a valid span_id then it SHOULD also
|
||||
// specify a valid trace_id.
|
||||
//
|
||||
// The receivers SHOULD assume that the log record is not associated with a
|
||||
// span if any of the following is true:
|
||||
// - the field is not present,
|
||||
// - the field contains an invalid value.
|
||||
bytes span_id = 10;
|
||||
|
||||
// A unique identifier of event category/type.
|
||||
// All events with the same event_name are expected to conform to the same
|
||||
// schema for both their attributes and their body.
|
||||
//
|
||||
// Recommended to be fully qualified and short (no longer than 256 characters).
|
||||
//
|
||||
// Presence of event_name on the log record identifies this record
|
||||
// as an event.
|
||||
//
|
||||
// [Optional].
|
||||
//
|
||||
// Status: [Development]
|
||||
string event_name = 12;
|
||||
}
|
||||
@@ -0,0 +1,719 @@
|
||||
// Copyright 2019, OpenTelemetry Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package opentelemetry.proto.metrics.v1;
|
||||
|
||||
import "opentelemetry/proto/common/v1/common.proto";
|
||||
import "opentelemetry/proto/resource/v1/resource.proto";
|
||||
|
||||
option csharp_namespace = "OpenTelemetry.Proto.Metrics.V1";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "io.opentelemetry.proto.metrics.v1";
|
||||
option java_outer_classname = "MetricsProto";
|
||||
option go_package = "go.opentelemetry.io/proto/otlp/metrics/v1";
|
||||
|
||||
// MetricsData represents the metrics data that can be stored in a persistent
|
||||
// storage, OR can be embedded by other protocols that transfer OTLP metrics
|
||||
// data but do not implement the OTLP protocol.
|
||||
//
|
||||
// MetricsData
|
||||
// └─── ResourceMetrics
|
||||
// ├── Resource
|
||||
// ├── SchemaURL
|
||||
// └── ScopeMetrics
|
||||
// ├── Scope
|
||||
// ├── SchemaURL
|
||||
// └── Metric
|
||||
// ├── Name
|
||||
// ├── Description
|
||||
// ├── Unit
|
||||
// └── data
|
||||
// ├── Gauge
|
||||
// ├── Sum
|
||||
// ├── Histogram
|
||||
// ├── ExponentialHistogram
|
||||
// └── Summary
|
||||
//
|
||||
// The main difference between this message and collector protocol is that
|
||||
// in this message there will not be any "control" or "metadata" specific to
|
||||
// OTLP protocol.
|
||||
//
|
||||
// When new fields are added into this message, the OTLP request MUST be updated
|
||||
// as well.
|
||||
message MetricsData {
|
||||
// An array of ResourceMetrics.
|
||||
// For data coming from a single resource this array will typically contain
|
||||
// one element. Intermediary nodes that receive data from multiple origins
|
||||
// typically batch the data before forwarding further and in that case this
|
||||
// array will contain multiple elements.
|
||||
repeated ResourceMetrics resource_metrics = 1;
|
||||
}
|
||||
|
||||
// A collection of ScopeMetrics from a Resource.
|
||||
message ResourceMetrics {
|
||||
reserved 1000;
|
||||
|
||||
// The resource for the metrics in this message.
|
||||
// If this field is not set then no resource info is known.
|
||||
opentelemetry.proto.resource.v1.Resource resource = 1;
|
||||
|
||||
// A list of metrics that originate from a resource.
|
||||
repeated ScopeMetrics scope_metrics = 2;
|
||||
|
||||
// The Schema URL, if known. This is the identifier of the Schema that the resource data
|
||||
// is recorded in. Notably, the last part of the URL path is the version number of the
|
||||
// schema: http[s]://server[:port]/path/<version>. To learn more about Schema URL see
|
||||
// https://opentelemetry.io/docs/specs/otel/schemas/#schema-url
|
||||
// This schema_url applies to the data in the "resource" field. It does not apply
|
||||
// to the data in the "scope_metrics" field which have their own schema_url field.
|
||||
string schema_url = 3;
|
||||
}
|
||||
|
||||
// A collection of Metrics produced by an Scope.
|
||||
message ScopeMetrics {
|
||||
// The instrumentation scope information for the metrics in this message.
|
||||
// Semantically when InstrumentationScope isn't set, it is equivalent with
|
||||
// an empty instrumentation scope name (unknown).
|
||||
opentelemetry.proto.common.v1.InstrumentationScope scope = 1;
|
||||
|
||||
// A list of metrics that originate from an instrumentation library.
|
||||
repeated Metric metrics = 2;
|
||||
|
||||
// The Schema URL, if known. This is the identifier of the Schema that the metric data
|
||||
// is recorded in. Notably, the last part of the URL path is the version number of the
|
||||
// schema: http[s]://server[:port]/path/<version>. To learn more about Schema URL see
|
||||
// https://opentelemetry.io/docs/specs/otel/schemas/#schema-url
|
||||
// This schema_url applies to all metrics in the "metrics" field.
|
||||
string schema_url = 3;
|
||||
}
|
||||
|
||||
// Defines a Metric which has one or more timeseries. The following is a
|
||||
// brief summary of the Metric data model. For more details, see:
|
||||
//
|
||||
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md
|
||||
//
|
||||
// The data model and relation between entities is shown in the
|
||||
// diagram below. Here, "DataPoint" is the term used to refer to any
|
||||
// one of the specific data point value types, and "points" is the term used
|
||||
// to refer to any one of the lists of points contained in the Metric.
|
||||
//
|
||||
// - Metric is composed of a metadata and data.
|
||||
// - Metadata part contains a name, description, unit.
|
||||
// - Data is one of the possible types (Sum, Gauge, Histogram, Summary).
|
||||
// - DataPoint contains timestamps, attributes, and one of the possible value type
|
||||
// fields.
|
||||
//
|
||||
// Metric
|
||||
// +------------+
|
||||
// |name |
|
||||
// |description |
|
||||
// |unit | +------------------------------------+
|
||||
// |data |---> |Gauge, Sum, Histogram, Summary, ... |
|
||||
// +------------+ +------------------------------------+
|
||||
//
|
||||
// Data [One of Gauge, Sum, Histogram, Summary, ...]
|
||||
// +-----------+
|
||||
// |... | // Metadata about the Data.
|
||||
// |points |--+
|
||||
// +-----------+ |
|
||||
// | +---------------------------+
|
||||
// | |DataPoint 1 |
|
||||
// v |+------+------+ +------+ |
|
||||
// +-----+ ||label |label |...|label | |
|
||||
// | 1 |-->||value1|value2|...|valueN| |
|
||||
// +-----+ |+------+------+ +------+ |
|
||||
// | . | |+-----+ |
|
||||
// | . | ||value| |
|
||||
// | . | |+-----+ |
|
||||
// | . | +---------------------------+
|
||||
// | . | .
|
||||
// | . | .
|
||||
// | . | .
|
||||
// | . | +---------------------------+
|
||||
// | . | |DataPoint M |
|
||||
// +-----+ |+------+------+ +------+ |
|
||||
// | M |-->||label |label |...|label | |
|
||||
// +-----+ ||value1|value2|...|valueN| |
|
||||
// |+------+------+ +------+ |
|
||||
// |+-----+ |
|
||||
// ||value| |
|
||||
// |+-----+ |
|
||||
// +---------------------------+
|
||||
//
|
||||
// Each distinct type of DataPoint represents the output of a specific
|
||||
// aggregation function, the result of applying the DataPoint's
|
||||
// associated function of to one or more measurements.
|
||||
//
|
||||
// All DataPoint types have three common fields:
|
||||
// - Attributes includes key-value pairs associated with the data point
|
||||
// - TimeUnixNano is required, set to the end time of the aggregation
|
||||
// - StartTimeUnixNano is optional, but strongly encouraged for DataPoints
|
||||
// having an AggregationTemporality field, as discussed below.
|
||||
//
|
||||
// Both TimeUnixNano and StartTimeUnixNano values are expressed as
|
||||
// UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970.
|
||||
//
|
||||
// # TimeUnixNano
|
||||
//
|
||||
// This field is required, having consistent interpretation across
|
||||
// DataPoint types. TimeUnixNano is the moment corresponding to when
|
||||
// the data point's aggregate value was captured.
|
||||
//
|
||||
// Data points with the 0 value for TimeUnixNano SHOULD be rejected
|
||||
// by consumers.
|
||||
//
|
||||
// # StartTimeUnixNano
|
||||
//
|
||||
// StartTimeUnixNano in general allows detecting when a sequence of
|
||||
// observations is unbroken. This field indicates to consumers the
|
||||
// start time for points with cumulative and delta
|
||||
// AggregationTemporality, and it should be included whenever possible
|
||||
// to support correct rate calculation. Although it may be omitted
|
||||
// when the start time is truly unknown, setting StartTimeUnixNano is
|
||||
// strongly encouraged.
|
||||
message Metric {
|
||||
reserved 4, 6, 8;
|
||||
|
||||
// name of the metric.
|
||||
string name = 1;
|
||||
|
||||
// description of the metric, which can be used in documentation.
|
||||
string description = 2;
|
||||
|
||||
// unit in which the metric value is reported. Follows the format
|
||||
// described by https://unitsofmeasure.org/ucum.html.
|
||||
string unit = 3;
|
||||
|
||||
// Data determines the aggregation type (if any) of the metric, what is the
|
||||
// reported value type for the data points, as well as the relatationship to
|
||||
// the time interval over which they are reported.
|
||||
oneof data {
|
||||
Gauge gauge = 5;
|
||||
Sum sum = 7;
|
||||
Histogram histogram = 9;
|
||||
ExponentialHistogram exponential_histogram = 10;
|
||||
Summary summary = 11;
|
||||
}
|
||||
|
||||
// Additional metadata attributes that describe the metric. [Optional].
|
||||
// Attributes are non-identifying.
|
||||
// Consumers SHOULD NOT need to be aware of these attributes.
|
||||
// These attributes MAY be used to encode information allowing
|
||||
// for lossless roundtrip translation to / from another data model.
|
||||
// Attribute keys MUST be unique (it is not allowed to have more than one
|
||||
// attribute with the same key).
|
||||
repeated opentelemetry.proto.common.v1.KeyValue metadata = 12;
|
||||
}
|
||||
|
||||
// Gauge represents the type of a scalar metric that always exports the
|
||||
// "current value" for every data point. It should be used for an "unknown"
|
||||
// aggregation.
|
||||
//
|
||||
// A Gauge does not support different aggregation temporalities. Given the
|
||||
// aggregation is unknown, points cannot be combined using the same
|
||||
// aggregation, regardless of aggregation temporalities. Therefore,
|
||||
// AggregationTemporality is not included. Consequently, this also means
|
||||
// "StartTimeUnixNano" is ignored for all data points.
|
||||
message Gauge {
|
||||
repeated NumberDataPoint data_points = 1;
|
||||
}
|
||||
|
||||
// Sum represents the type of a scalar metric that is calculated as a sum of all
|
||||
// reported measurements over a time interval.
|
||||
message Sum {
|
||||
repeated NumberDataPoint data_points = 1;
|
||||
|
||||
// aggregation_temporality describes if the aggregator reports delta changes
|
||||
// since last report time, or cumulative changes since a fixed start time.
|
||||
AggregationTemporality aggregation_temporality = 2;
|
||||
|
||||
// If "true" means that the sum is monotonic.
|
||||
bool is_monotonic = 3;
|
||||
}
|
||||
|
||||
// Histogram represents the type of a metric that is calculated by aggregating
|
||||
// as a Histogram of all reported measurements over a time interval.
|
||||
message Histogram {
|
||||
repeated HistogramDataPoint data_points = 1;
|
||||
|
||||
// aggregation_temporality describes if the aggregator reports delta changes
|
||||
// since last report time, or cumulative changes since a fixed start time.
|
||||
AggregationTemporality aggregation_temporality = 2;
|
||||
}
|
||||
|
||||
// ExponentialHistogram represents the type of a metric that is calculated by aggregating
|
||||
// as a ExponentialHistogram of all reported double measurements over a time interval.
|
||||
message ExponentialHistogram {
|
||||
repeated ExponentialHistogramDataPoint data_points = 1;
|
||||
|
||||
// aggregation_temporality describes if the aggregator reports delta changes
|
||||
// since last report time, or cumulative changes since a fixed start time.
|
||||
AggregationTemporality aggregation_temporality = 2;
|
||||
}
|
||||
|
||||
// Summary metric data are used to convey quantile summaries,
|
||||
// a Prometheus (see: https://prometheus.io/docs/concepts/metric_types/#summary)
|
||||
// and OpenMetrics (see: https://github.com/prometheus/OpenMetrics/blob/4dbf6075567ab43296eed941037c12951faafb92/protos/prometheus.proto#L45)
|
||||
// data type. These data points cannot always be merged in a meaningful way.
|
||||
// While they can be useful in some applications, histogram data points are
|
||||
// recommended for new applications.
|
||||
// Summary metrics do not have an aggregation temporality field. This is
|
||||
// because the count and sum fields of a SummaryDataPoint are assumed to be
|
||||
// cumulative values.
|
||||
message Summary {
|
||||
repeated SummaryDataPoint data_points = 1;
|
||||
}
|
||||
|
||||
// AggregationTemporality defines how a metric aggregator reports aggregated
|
||||
// values. It describes how those values relate to the time interval over
|
||||
// which they are aggregated.
|
||||
enum AggregationTemporality {
|
||||
// UNSPECIFIED is the default AggregationTemporality, it MUST not be used.
|
||||
AGGREGATION_TEMPORALITY_UNSPECIFIED = 0;
|
||||
|
||||
// DELTA is an AggregationTemporality for a metric aggregator which reports
|
||||
// changes since last report time. Successive metrics contain aggregation of
|
||||
// values from continuous and non-overlapping intervals.
|
||||
//
|
||||
// The values for a DELTA metric are based only on the time interval
|
||||
// associated with one measurement cycle. There is no dependency on
|
||||
// previous measurements like is the case for CUMULATIVE metrics.
|
||||
//
|
||||
// For example, consider a system measuring the number of requests that
|
||||
// it receives and reports the sum of these requests every second as a
|
||||
// DELTA metric:
|
||||
//
|
||||
// 1. The system starts receiving at time=t_0.
|
||||
// 2. A request is received, the system measures 1 request.
|
||||
// 3. A request is received, the system measures 1 request.
|
||||
// 4. A request is received, the system measures 1 request.
|
||||
// 5. The 1 second collection cycle ends. A metric is exported for the
|
||||
// number of requests received over the interval of time t_0 to
|
||||
// t_0+1 with a value of 3.
|
||||
// 6. A request is received, the system measures 1 request.
|
||||
// 7. A request is received, the system measures 1 request.
|
||||
// 8. The 1 second collection cycle ends. A metric is exported for the
|
||||
// number of requests received over the interval of time t_0+1 to
|
||||
// t_0+2 with a value of 2.
|
||||
AGGREGATION_TEMPORALITY_DELTA = 1;
|
||||
|
||||
// CUMULATIVE is an AggregationTemporality for a metric aggregator which
|
||||
// reports changes since a fixed start time. This means that current values
|
||||
// of a CUMULATIVE metric depend on all previous measurements since the
|
||||
// start time. Because of this, the sender is required to retain this state
|
||||
// in some form. If this state is lost or invalidated, the CUMULATIVE metric
|
||||
// values MUST be reset and a new fixed start time following the last
|
||||
// reported measurement time sent MUST be used.
|
||||
//
|
||||
// For example, consider a system measuring the number of requests that
|
||||
// it receives and reports the sum of these requests every second as a
|
||||
// CUMULATIVE metric:
|
||||
//
|
||||
// 1. The system starts receiving at time=t_0.
|
||||
// 2. A request is received, the system measures 1 request.
|
||||
// 3. A request is received, the system measures 1 request.
|
||||
// 4. A request is received, the system measures 1 request.
|
||||
// 5. The 1 second collection cycle ends. A metric is exported for the
|
||||
// number of requests received over the interval of time t_0 to
|
||||
// t_0+1 with a value of 3.
|
||||
// 6. A request is received, the system measures 1 request.
|
||||
// 7. A request is received, the system measures 1 request.
|
||||
// 8. The 1 second collection cycle ends. A metric is exported for the
|
||||
// number of requests received over the interval of time t_0 to
|
||||
// t_0+2 with a value of 5.
|
||||
// 9. The system experiences a fault and loses state.
|
||||
// 10. The system recovers and resumes receiving at time=t_1.
|
||||
// 11. A request is received, the system measures 1 request.
|
||||
// 12. The 1 second collection cycle ends. A metric is exported for the
|
||||
// number of requests received over the interval of time t_1 to
|
||||
// t_0+1 with a value of 1.
|
||||
//
|
||||
// Note: Even though, when reporting changes since last report time, using
|
||||
// CUMULATIVE is valid, it is not recommended. This may cause problems for
|
||||
// systems that do not use start_time to determine when the aggregation
|
||||
// value was reset (e.g. Prometheus).
|
||||
AGGREGATION_TEMPORALITY_CUMULATIVE = 2;
|
||||
}
|
||||
|
||||
// DataPointFlags is defined as a protobuf 'uint32' type and is to be used as a
|
||||
// bit-field representing 32 distinct boolean flags. Each flag defined in this
|
||||
// enum is a bit-mask. To test the presence of a single flag in the flags of
|
||||
// a data point, for example, use an expression like:
|
||||
//
|
||||
// (point.flags & DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK) == DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK
|
||||
//
|
||||
enum DataPointFlags {
|
||||
// The zero value for the enum. Should not be used for comparisons.
|
||||
// Instead use bitwise "and" with the appropriate mask as shown above.
|
||||
DATA_POINT_FLAGS_DO_NOT_USE = 0;
|
||||
|
||||
// This DataPoint is valid but has no recorded value. This value
|
||||
// SHOULD be used to reflect explicitly missing data in a series, as
|
||||
// for an equivalent to the Prometheus "staleness marker".
|
||||
DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK = 1;
|
||||
|
||||
// Bits 2-31 are reserved for future use.
|
||||
}
|
||||
|
||||
// NumberDataPoint is a single data point in a timeseries that describes the
|
||||
// time-varying scalar value of a metric.
|
||||
message NumberDataPoint {
|
||||
reserved 1;
|
||||
|
||||
// The set of key/value pairs that uniquely identify the timeseries from
|
||||
// where this point belongs. The list may be empty (may contain 0 elements).
|
||||
// Attribute keys MUST be unique (it is not allowed to have more than one
|
||||
// attribute with the same key).
|
||||
repeated opentelemetry.proto.common.v1.KeyValue attributes = 7;
|
||||
|
||||
// StartTimeUnixNano is optional but strongly encouraged, see the
|
||||
// the detailed comments above Metric.
|
||||
//
|
||||
// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January
|
||||
// 1970.
|
||||
fixed64 start_time_unix_nano = 2;
|
||||
|
||||
// TimeUnixNano is required, see the detailed comments above Metric.
|
||||
//
|
||||
// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January
|
||||
// 1970.
|
||||
fixed64 time_unix_nano = 3;
|
||||
|
||||
// The value itself. A point is considered invalid when one of the recognized
|
||||
// value fields is not present inside this oneof.
|
||||
oneof value {
|
||||
double as_double = 4;
|
||||
sfixed64 as_int = 6;
|
||||
}
|
||||
|
||||
// (Optional) List of exemplars collected from
|
||||
// measurements that were used to form the data point
|
||||
repeated Exemplar exemplars = 5;
|
||||
|
||||
// Flags that apply to this specific data point. See DataPointFlags
|
||||
// for the available flags and their meaning.
|
||||
uint32 flags = 8;
|
||||
}
|
||||
|
||||
// HistogramDataPoint is a single data point in a timeseries that describes the
|
||||
// time-varying values of a Histogram. A Histogram contains summary statistics
|
||||
// for a population of values, it may optionally contain the distribution of
|
||||
// those values across a set of buckets.
|
||||
//
|
||||
// If the histogram contains the distribution of values, then both
|
||||
// "explicit_bounds" and "bucket counts" fields must be defined.
|
||||
// If the histogram does not contain the distribution of values, then both
|
||||
// "explicit_bounds" and "bucket_counts" must be omitted and only "count" and
|
||||
// "sum" are known.
|
||||
message HistogramDataPoint {
|
||||
reserved 1;
|
||||
|
||||
// The set of key/value pairs that uniquely identify the timeseries from
|
||||
// where this point belongs. The list may be empty (may contain 0 elements).
|
||||
// Attribute keys MUST be unique (it is not allowed to have more than one
|
||||
// attribute with the same key).
|
||||
repeated opentelemetry.proto.common.v1.KeyValue attributes = 9;
|
||||
|
||||
// StartTimeUnixNano is optional but strongly encouraged, see the
|
||||
// the detailed comments above Metric.
|
||||
//
|
||||
// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January
|
||||
// 1970.
|
||||
fixed64 start_time_unix_nano = 2;
|
||||
|
||||
// TimeUnixNano is required, see the detailed comments above Metric.
|
||||
//
|
||||
// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January
|
||||
// 1970.
|
||||
fixed64 time_unix_nano = 3;
|
||||
|
||||
// count is the number of values in the population. Must be non-negative. This
|
||||
// value must be equal to the sum of the "count" fields in buckets if a
|
||||
// histogram is provided.
|
||||
fixed64 count = 4;
|
||||
|
||||
// sum of the values in the population. If count is zero then this field
|
||||
// must be zero.
|
||||
//
|
||||
// Note: Sum should only be filled out when measuring non-negative discrete
|
||||
// events, and is assumed to be monotonic over the values of these events.
|
||||
// Negative events *can* be recorded, but sum should not be filled out when
|
||||
// doing so. This is specifically to enforce compatibility w/ OpenMetrics,
|
||||
// see: https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#histogram
|
||||
optional double sum = 5;
|
||||
|
||||
// bucket_counts is an optional field contains the count values of histogram
|
||||
// for each bucket.
|
||||
//
|
||||
// The sum of the bucket_counts must equal the value in the count field.
|
||||
//
|
||||
// The number of elements in bucket_counts array must be by one greater than
|
||||
// the number of elements in explicit_bounds array. The exception to this rule
|
||||
// is when the length of bucket_counts is 0, then the length of explicit_bounds
|
||||
// must also be 0.
|
||||
repeated fixed64 bucket_counts = 6;
|
||||
|
||||
// explicit_bounds specifies buckets with explicitly defined bounds for values.
|
||||
//
|
||||
// The boundaries for bucket at index i are:
|
||||
//
|
||||
// (-infinity, explicit_bounds[i]] for i == 0
|
||||
// (explicit_bounds[i-1], explicit_bounds[i]] for 0 < i < size(explicit_bounds)
|
||||
// (explicit_bounds[i-1], +infinity) for i == size(explicit_bounds)
|
||||
//
|
||||
// The values in the explicit_bounds array must be strictly increasing.
|
||||
//
|
||||
// Histogram buckets are inclusive of their upper boundary, except the last
|
||||
// bucket where the boundary is at infinity. This format is intentionally
|
||||
// compatible with the OpenMetrics histogram definition.
|
||||
//
|
||||
// If bucket_counts length is 0 then explicit_bounds length must also be 0,
|
||||
// otherwise the data point is invalid.
|
||||
repeated double explicit_bounds = 7;
|
||||
|
||||
// (Optional) List of exemplars collected from
|
||||
// measurements that were used to form the data point
|
||||
repeated Exemplar exemplars = 8;
|
||||
|
||||
// Flags that apply to this specific data point. See DataPointFlags
|
||||
// for the available flags and their meaning.
|
||||
uint32 flags = 10;
|
||||
|
||||
// min is the minimum value over (start_time, end_time].
|
||||
optional double min = 11;
|
||||
|
||||
// max is the maximum value over (start_time, end_time].
|
||||
optional double max = 12;
|
||||
}
|
||||
|
||||
// ExponentialHistogramDataPoint is a single data point in a timeseries that describes the
|
||||
// time-varying values of a ExponentialHistogram of double values. A ExponentialHistogram contains
|
||||
// summary statistics for a population of values, it may optionally contain the
|
||||
// distribution of those values across a set of buckets.
|
||||
//
|
||||
message ExponentialHistogramDataPoint {
|
||||
// The set of key/value pairs that uniquely identify the timeseries from
|
||||
// where this point belongs. The list may be empty (may contain 0 elements).
|
||||
// Attribute keys MUST be unique (it is not allowed to have more than one
|
||||
// attribute with the same key).
|
||||
repeated opentelemetry.proto.common.v1.KeyValue attributes = 1;
|
||||
|
||||
// StartTimeUnixNano is optional but strongly encouraged, see the
|
||||
// the detailed comments above Metric.
|
||||
//
|
||||
// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January
|
||||
// 1970.
|
||||
fixed64 start_time_unix_nano = 2;
|
||||
|
||||
// TimeUnixNano is required, see the detailed comments above Metric.
|
||||
//
|
||||
// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January
|
||||
// 1970.
|
||||
fixed64 time_unix_nano = 3;
|
||||
|
||||
// count is the number of values in the population. Must be
|
||||
// non-negative. This value must be equal to the sum of the "bucket_counts"
|
||||
// values in the positive and negative Buckets plus the "zero_count" field.
|
||||
fixed64 count = 4;
|
||||
|
||||
// sum of the values in the population. If count is zero then this field
|
||||
// must be zero.
|
||||
//
|
||||
// Note: Sum should only be filled out when measuring non-negative discrete
|
||||
// events, and is assumed to be monotonic over the values of these events.
|
||||
// Negative events *can* be recorded, but sum should not be filled out when
|
||||
// doing so. This is specifically to enforce compatibility w/ OpenMetrics,
|
||||
// see: https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#histogram
|
||||
optional double sum = 5;
|
||||
|
||||
// scale describes the resolution of the histogram. Boundaries are
|
||||
// located at powers of the base, where:
|
||||
//
|
||||
// base = (2^(2^-scale))
|
||||
//
|
||||
// The histogram bucket identified by `index`, a signed integer,
|
||||
// contains values that are greater than (base^index) and
|
||||
// less than or equal to (base^(index+1)).
|
||||
//
|
||||
// The positive and negative ranges of the histogram are expressed
|
||||
// separately. Negative values are mapped by their absolute value
|
||||
// into the negative range using the same scale as the positive range.
|
||||
//
|
||||
// scale is not restricted by the protocol, as the permissible
|
||||
// values depend on the range of the data.
|
||||
sint32 scale = 6;
|
||||
|
||||
// zero_count is the count of values that are either exactly zero or
|
||||
// within the region considered zero by the instrumentation at the
|
||||
// tolerated degree of precision. This bucket stores values that
|
||||
// cannot be expressed using the standard exponential formula as
|
||||
// well as values that have been rounded to zero.
|
||||
//
|
||||
// Implementations MAY consider the zero bucket to have probability
|
||||
// mass equal to (zero_count / count).
|
||||
fixed64 zero_count = 7;
|
||||
|
||||
// positive carries the positive range of exponential bucket counts.
|
||||
Buckets positive = 8;
|
||||
|
||||
// negative carries the negative range of exponential bucket counts.
|
||||
Buckets negative = 9;
|
||||
|
||||
// Buckets are a set of bucket counts, encoded in a contiguous array
|
||||
// of counts.
|
||||
message Buckets {
|
||||
// Offset is the bucket index of the first entry in the bucket_counts array.
|
||||
//
|
||||
// Note: This uses a varint encoding as a simple form of compression.
|
||||
sint32 offset = 1;
|
||||
|
||||
// bucket_counts is an array of count values, where bucket_counts[i] carries
|
||||
// the count of the bucket at index (offset+i). bucket_counts[i] is the count
|
||||
// of values greater than base^(offset+i) and less than or equal to
|
||||
// base^(offset+i+1).
|
||||
//
|
||||
// Note: By contrast, the explicit HistogramDataPoint uses
|
||||
// fixed64. This field is expected to have many buckets,
|
||||
// especially zeros, so uint64 has been selected to ensure
|
||||
// varint encoding.
|
||||
repeated uint64 bucket_counts = 2;
|
||||
}
|
||||
|
||||
// Flags that apply to this specific data point. See DataPointFlags
|
||||
// for the available flags and their meaning.
|
||||
uint32 flags = 10;
|
||||
|
||||
// (Optional) List of exemplars collected from
|
||||
// measurements that were used to form the data point
|
||||
repeated Exemplar exemplars = 11;
|
||||
|
||||
// min is the minimum value over (start_time, end_time].
|
||||
optional double min = 12;
|
||||
|
||||
// max is the maximum value over (start_time, end_time].
|
||||
optional double max = 13;
|
||||
|
||||
// ZeroThreshold may be optionally set to convey the width of the zero
|
||||
// region. Where the zero region is defined as the closed interval
|
||||
// [-ZeroThreshold, ZeroThreshold].
|
||||
// When ZeroThreshold is 0, zero count bucket stores values that cannot be
|
||||
// expressed using the standard exponential formula as well as values that
|
||||
// have been rounded to zero.
|
||||
double zero_threshold = 14;
|
||||
}
|
||||
|
||||
// SummaryDataPoint is a single data point in a timeseries that describes the
|
||||
// time-varying values of a Summary metric. The count and sum fields represent
|
||||
// cumulative values.
|
||||
message SummaryDataPoint {
|
||||
reserved 1;
|
||||
|
||||
// The set of key/value pairs that uniquely identify the timeseries from
|
||||
// where this point belongs. The list may be empty (may contain 0 elements).
|
||||
// Attribute keys MUST be unique (it is not allowed to have more than one
|
||||
// attribute with the same key).
|
||||
repeated opentelemetry.proto.common.v1.KeyValue attributes = 7;
|
||||
|
||||
// StartTimeUnixNano is optional but strongly encouraged, see the
|
||||
// the detailed comments above Metric.
|
||||
//
|
||||
// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January
|
||||
// 1970.
|
||||
fixed64 start_time_unix_nano = 2;
|
||||
|
||||
// TimeUnixNano is required, see the detailed comments above Metric.
|
||||
//
|
||||
// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January
|
||||
// 1970.
|
||||
fixed64 time_unix_nano = 3;
|
||||
|
||||
// count is the number of values in the population. Must be non-negative.
|
||||
fixed64 count = 4;
|
||||
|
||||
// sum of the values in the population. If count is zero then this field
|
||||
// must be zero.
|
||||
//
|
||||
// Note: Sum should only be filled out when measuring non-negative discrete
|
||||
// events, and is assumed to be monotonic over the values of these events.
|
||||
// Negative events *can* be recorded, but sum should not be filled out when
|
||||
// doing so. This is specifically to enforce compatibility w/ OpenMetrics,
|
||||
// see: https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#summary
|
||||
double sum = 5;
|
||||
|
||||
// Represents the value at a given quantile of a distribution.
|
||||
//
|
||||
// To record Min and Max values following conventions are used:
|
||||
// - The 1.0 quantile is equivalent to the maximum value observed.
|
||||
// - The 0.0 quantile is equivalent to the minimum value observed.
|
||||
//
|
||||
// See the following issue for more context:
|
||||
// https://github.com/open-telemetry/opentelemetry-proto/issues/125
|
||||
message ValueAtQuantile {
|
||||
// The quantile of a distribution. Must be in the interval
|
||||
// [0.0, 1.0].
|
||||
double quantile = 1;
|
||||
|
||||
// The value at the given quantile of a distribution.
|
||||
//
|
||||
// Quantile values must NOT be negative.
|
||||
double value = 2;
|
||||
}
|
||||
|
||||
// (Optional) list of values at different quantiles of the distribution calculated
|
||||
// from the current snapshot. The quantiles must be strictly increasing.
|
||||
repeated ValueAtQuantile quantile_values = 6;
|
||||
|
||||
// Flags that apply to this specific data point. See DataPointFlags
|
||||
// for the available flags and their meaning.
|
||||
uint32 flags = 8;
|
||||
}
|
||||
|
||||
// A representation of an exemplar, which is a sample input measurement.
|
||||
// Exemplars also hold information about the environment when the measurement
|
||||
// was recorded, for example the span and trace ID of the active span when the
|
||||
// exemplar was recorded.
|
||||
message Exemplar {
|
||||
reserved 1;
|
||||
|
||||
// The set of key/value pairs that were filtered out by the aggregator, but
|
||||
// recorded alongside the original measurement. Only key/value pairs that were
|
||||
// filtered out by the aggregator should be included
|
||||
repeated opentelemetry.proto.common.v1.KeyValue filtered_attributes = 7;
|
||||
|
||||
// time_unix_nano is the exact time when this exemplar was recorded
|
||||
//
|
||||
// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January
|
||||
// 1970.
|
||||
fixed64 time_unix_nano = 2;
|
||||
|
||||
// The value of the measurement that was recorded. An exemplar is
|
||||
// considered invalid when one of the recognized value fields is not present
|
||||
// inside this oneof.
|
||||
oneof value {
|
||||
double as_double = 3;
|
||||
sfixed64 as_int = 6;
|
||||
}
|
||||
|
||||
// (Optional) Span ID of the exemplar trace.
|
||||
// span_id may be missing if the measurement is not recorded inside a trace
|
||||
// or if the trace is not sampled.
|
||||
bytes span_id = 4;
|
||||
|
||||
// (Optional) Trace ID of the exemplar trace.
|
||||
// trace_id may be missing if the measurement is not recorded inside a trace
|
||||
// or if the trace is not sampled.
|
||||
bytes trace_id = 5;
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
// Copyright 2023, OpenTelemetry Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// This file includes work covered by the following copyright and permission notices:
|
||||
//
|
||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package opentelemetry.proto.profiles.v1development;
|
||||
|
||||
import "opentelemetry/proto/common/v1/common.proto";
|
||||
import "opentelemetry/proto/resource/v1/resource.proto";
|
||||
|
||||
option csharp_namespace = "OpenTelemetry.Proto.Profiles.V1Development";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "io.opentelemetry.proto.profiles.v1development";
|
||||
option java_outer_classname = "ProfilesProto";
|
||||
option go_package = "go.opentelemetry.io/proto/otlp/profiles/v1development";
|
||||
|
||||
// Relationships Diagram
|
||||
//
|
||||
// ┌──────────────────┐ LEGEND
|
||||
// │ ProfilesData │
|
||||
// └──────────────────┘ ─────▶ embedded
|
||||
// │
|
||||
// │ 1-n ─────▷ referenced by index
|
||||
// ▼
|
||||
// ┌──────────────────┐
|
||||
// │ ResourceProfiles │
|
||||
// └──────────────────┘
|
||||
// │
|
||||
// │ 1-n
|
||||
// ▼
|
||||
// ┌──────────────────┐
|
||||
// │ ScopeProfiles │
|
||||
// └──────────────────┘
|
||||
// │
|
||||
// │ 1-1
|
||||
// ▼
|
||||
// ┌──────────────────┐
|
||||
// │ Profile │
|
||||
// └──────────────────┘
|
||||
// │ n-1
|
||||
// │ 1-n ┌───────────────────────────────────────┐
|
||||
// ▼ │ ▽
|
||||
// ┌──────────────────┐ 1-n ┌──────────────┐ ┌──────────┐
|
||||
// │ Sample │ ──────▷ │ KeyValue │ │ Link │
|
||||
// └──────────────────┘ └──────────────┘ └──────────┘
|
||||
// │ 1-n △ △
|
||||
// │ 1-n ┌─────────────────┘ │ 1-n
|
||||
// ▽ │ │
|
||||
// ┌──────────────────┐ n-1 ┌──────────────┐
|
||||
// │ Location │ ──────▷ │ Mapping │
|
||||
// └──────────────────┘ └──────────────┘
|
||||
// │
|
||||
// │ 1-n
|
||||
// ▼
|
||||
// ┌──────────────────┐
|
||||
// │ Line │
|
||||
// └──────────────────┘
|
||||
// │
|
||||
// │ 1-1
|
||||
// ▽
|
||||
// ┌──────────────────┐
|
||||
// │ Function │
|
||||
// └──────────────────┘
|
||||
//
|
||||
|
||||
// ProfilesData represents the profiles data that can be stored in persistent storage,
|
||||
// OR can be embedded by other protocols that transfer OTLP profiles data but do not
|
||||
// implement the OTLP protocol.
|
||||
//
|
||||
// The main difference between this message and collector protocol is that
|
||||
// in this message there will not be any "control" or "metadata" specific to
|
||||
// OTLP protocol.
|
||||
//
|
||||
// When new fields are added into this message, the OTLP request MUST be updated
|
||||
// as well.
|
||||
message ProfilesData {
|
||||
// An array of ResourceProfiles.
|
||||
// For data coming from a single resource this array will typically contain
|
||||
// one element. Intermediary nodes that receive data from multiple origins
|
||||
// typically batch the data before forwarding further and in that case this
|
||||
// array will contain multiple elements.
|
||||
repeated ResourceProfiles resource_profiles = 1;
|
||||
}
|
||||
|
||||
|
||||
// A collection of ScopeProfiles from a Resource.
|
||||
message ResourceProfiles {
|
||||
reserved 1000;
|
||||
|
||||
// The resource for the profiles in this message.
|
||||
// If this field is not set then no resource info is known.
|
||||
opentelemetry.proto.resource.v1.Resource resource = 1;
|
||||
|
||||
// A list of ScopeProfiles that originate from a resource.
|
||||
repeated ScopeProfiles scope_profiles = 2;
|
||||
|
||||
// The Schema URL, if known. This is the identifier of the Schema that the resource data
|
||||
// is recorded in. Notably, the last part of the URL path is the version number of the
|
||||
// schema: http[s]://server[:port]/path/<version>. To learn more about Schema URL see
|
||||
// https://opentelemetry.io/docs/specs/otel/schemas/#schema-url
|
||||
// This schema_url applies to the data in the "resource" field. It does not apply
|
||||
// to the data in the "scope_profiles" field which have their own schema_url field.
|
||||
string schema_url = 3;
|
||||
}
|
||||
|
||||
// A collection of Profiles produced by an InstrumentationScope.
|
||||
message ScopeProfiles {
|
||||
// The instrumentation scope information for the profiles in this message.
|
||||
// Semantically when InstrumentationScope isn't set, it is equivalent with
|
||||
// an empty instrumentation scope name (unknown).
|
||||
opentelemetry.proto.common.v1.InstrumentationScope scope = 1;
|
||||
|
||||
// A list of Profiles that originate from an instrumentation scope.
|
||||
repeated Profile profiles = 2;
|
||||
|
||||
// The Schema URL, if known. This is the identifier of the Schema that the profile data
|
||||
// is recorded in. Notably, the last part of the URL path is the version number of the
|
||||
// schema: http[s]://server[:port]/path/<version>. To learn more about Schema URL see
|
||||
// https://opentelemetry.io/docs/specs/otel/schemas/#schema-url
|
||||
// This schema_url applies to all profiles in the "profiles" field.
|
||||
string schema_url = 3;
|
||||
}
|
||||
|
||||
// Profile is a common stacktrace profile format.
|
||||
//
|
||||
// Measurements represented with this format should follow the
|
||||
// following conventions:
|
||||
//
|
||||
// - Consumers should treat unset optional fields as if they had been
|
||||
// set with their default value.
|
||||
//
|
||||
// - When possible, measurements should be stored in "unsampled" form
|
||||
// that is most useful to humans. There should be enough
|
||||
// information present to determine the original sampled values.
|
||||
//
|
||||
// - On-disk, the serialized proto must be gzip-compressed.
|
||||
//
|
||||
// - The profile is represented as a set of samples, where each sample
|
||||
// references a sequence of locations, and where each location belongs
|
||||
// to a mapping.
|
||||
// - There is a N->1 relationship from sample.location_id entries to
|
||||
// locations. For every sample.location_id entry there must be a
|
||||
// unique Location with that index.
|
||||
// - There is an optional N->1 relationship from locations to
|
||||
// mappings. For every nonzero Location.mapping_id there must be a
|
||||
// unique Mapping with that index.
|
||||
|
||||
// Represents a complete profile, including sample types, samples,
|
||||
// mappings to binaries, locations, functions, string table, and additional metadata.
|
||||
// It modifies and annotates pprof Profile with OpenTelemetry specific fields.
|
||||
//
|
||||
// Note that whilst fields in this message retain the name and field id from pprof in most cases
|
||||
// for ease of understanding data migration, it is not intended that pprof:Profile and
|
||||
// OpenTelemetry:Profile encoding be wire compatible.
|
||||
message Profile {
|
||||
|
||||
// A description of the samples associated with each Sample.value.
|
||||
// For a cpu profile this might be:
|
||||
// [["cpu","nanoseconds"]] or [["wall","seconds"]] or [["syscall","count"]]
|
||||
// For a heap profile, this might be:
|
||||
// [["allocations","count"], ["space","bytes"]],
|
||||
// If one of the values represents the number of events represented
|
||||
// by the sample, by convention it should be at index 0 and use
|
||||
// sample_type.unit == "count".
|
||||
repeated ValueType sample_type = 1;
|
||||
// The set of samples recorded in this profile.
|
||||
repeated Sample sample = 2;
|
||||
// Mapping from address ranges to the image/binary/library mapped
|
||||
// into that address range. mapping[0] will be the main binary.
|
||||
// If multiple binaries contribute to the Profile and no main
|
||||
// binary can be identified, mapping[0] has no special meaning.
|
||||
repeated Mapping mapping_table = 3;
|
||||
// Locations referenced by samples via location_indices.
|
||||
repeated Location location_table = 4;
|
||||
// Array of locations referenced by samples.
|
||||
repeated int32 location_indices = 5;
|
||||
// Functions referenced by locations.
|
||||
repeated Function function_table = 6;
|
||||
// Lookup table for attributes.
|
||||
repeated opentelemetry.proto.common.v1.KeyValue attribute_table = 7;
|
||||
// Represents a mapping between Attribute Keys and Units.
|
||||
repeated AttributeUnit attribute_units = 8;
|
||||
// Lookup table for links.
|
||||
repeated Link link_table = 9;
|
||||
// A common table for strings referenced by various messages.
|
||||
// string_table[0] must always be "".
|
||||
repeated string string_table = 10;
|
||||
|
||||
// The following fields 9-14 are informational, do not affect
|
||||
// interpretation of results.
|
||||
|
||||
// Time of collection (UTC) represented as nanoseconds past the epoch.
|
||||
int64 time_nanos = 11;
|
||||
// Duration of the profile, if a duration makes sense.
|
||||
int64 duration_nanos = 12;
|
||||
// The kind of events between sampled occurrences.
|
||||
// e.g [ "cpu","cycles" ] or [ "heap","bytes" ]
|
||||
ValueType period_type = 13;
|
||||
// The number of events between sampled occurrences.
|
||||
int64 period = 14;
|
||||
// Free-form text associated with the profile. The text is displayed as is
|
||||
// to the user by the tools that read profiles (e.g. by pprof). This field
|
||||
// should not be used to store any machine-readable information, it is only
|
||||
// for human-friendly content. The profile must stay functional if this field
|
||||
// is cleaned.
|
||||
repeated int32 comment_strindices = 15; // Indices into string table.
|
||||
// Index into the sample_type array to the default sample type.
|
||||
int32 default_sample_type_index = 16;
|
||||
|
||||
|
||||
// A globally unique identifier for a profile. The ID is a 16-byte array. An ID with
|
||||
// all zeroes is considered invalid.
|
||||
//
|
||||
// This field is required.
|
||||
bytes profile_id = 17;
|
||||
|
||||
// dropped_attributes_count is the number of attributes that were discarded. Attributes
|
||||
// can be discarded because their keys are too long or because there are too many
|
||||
// attributes. If this value is 0, then no attributes were dropped.
|
||||
uint32 dropped_attributes_count = 19;
|
||||
|
||||
// Specifies format of the original payload. Common values are defined in semantic conventions. [required if original_payload is present]
|
||||
string original_payload_format = 20;
|
||||
|
||||
// Original payload can be stored in this field. This can be useful for users who want to get the original payload.
|
||||
// Formats such as JFR are highly extensible and can contain more information than what is defined in this spec.
|
||||
// Inclusion of original payload should be configurable by the user. Default behavior should be to not include the original payload.
|
||||
// If the original payload is in pprof format, it SHOULD not be included in this field.
|
||||
// The field is optional, however if it is present then equivalent converted data should be populated in other fields
|
||||
// of this message as far as is practicable.
|
||||
bytes original_payload = 21;
|
||||
|
||||
// References to attributes in attribute_table. [optional]
|
||||
// It is a collection of key/value pairs. Note, global attributes
|
||||
// like server name can be set using the resource API. Examples of attributes:
|
||||
//
|
||||
// "/http/user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"
|
||||
// "/http/server_latency": 300
|
||||
// "abc.com/myattribute": true
|
||||
// "abc.com/score": 10.239
|
||||
//
|
||||
// The OpenTelemetry API specification further restricts the allowed value types:
|
||||
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/common/README.md#attribute
|
||||
// Attribute keys MUST be unique (it is not allowed to have more than one
|
||||
// attribute with the same key).
|
||||
repeated int32 attribute_indices = 22;
|
||||
}
|
||||
|
||||
// Represents a mapping between Attribute Keys and Units.
|
||||
message AttributeUnit {
|
||||
// Index into string table.
|
||||
int32 attribute_key_strindex = 1;
|
||||
// Index into string table.
|
||||
int32 unit_strindex = 2;
|
||||
}
|
||||
|
||||
// A pointer from a profile Sample to a trace Span.
|
||||
// Connects a profile sample to a trace span, identified by unique trace and span IDs.
|
||||
message Link {
|
||||
// A unique identifier of a trace that this linked span is part of. The ID is a
|
||||
// 16-byte array.
|
||||
bytes trace_id = 1;
|
||||
|
||||
// A unique identifier for the linked span. The ID is an 8-byte array.
|
||||
bytes span_id = 2;
|
||||
}
|
||||
|
||||
// Specifies the method of aggregating metric values, either DELTA (change since last report)
|
||||
// or CUMULATIVE (total since a fixed start time).
|
||||
enum AggregationTemporality {
|
||||
/* UNSPECIFIED is the default AggregationTemporality, it MUST not be used. */
|
||||
AGGREGATION_TEMPORALITY_UNSPECIFIED = 0;
|
||||
|
||||
/** DELTA is an AggregationTemporality for a profiler which reports
|
||||
changes since last report time. Successive metrics contain aggregation of
|
||||
values from continuous and non-overlapping intervals.
|
||||
|
||||
The values for a DELTA metric are based only on the time interval
|
||||
associated with one measurement cycle. There is no dependency on
|
||||
previous measurements like is the case for CUMULATIVE metrics.
|
||||
|
||||
For example, consider a system measuring the number of requests that
|
||||
it receives and reports the sum of these requests every second as a
|
||||
DELTA metric:
|
||||
|
||||
1. The system starts receiving at time=t_0.
|
||||
2. A request is received, the system measures 1 request.
|
||||
3. A request is received, the system measures 1 request.
|
||||
4. A request is received, the system measures 1 request.
|
||||
5. The 1 second collection cycle ends. A metric is exported for the
|
||||
number of requests received over the interval of time t_0 to
|
||||
t_0+1 with a value of 3.
|
||||
6. A request is received, the system measures 1 request.
|
||||
7. A request is received, the system measures 1 request.
|
||||
8. The 1 second collection cycle ends. A metric is exported for the
|
||||
number of requests received over the interval of time t_0+1 to
|
||||
t_0+2 with a value of 2. */
|
||||
AGGREGATION_TEMPORALITY_DELTA = 1;
|
||||
|
||||
/** CUMULATIVE is an AggregationTemporality for a profiler which
|
||||
reports changes since a fixed start time. This means that current values
|
||||
of a CUMULATIVE metric depend on all previous measurements since the
|
||||
start time. Because of this, the sender is required to retain this state
|
||||
in some form. If this state is lost or invalidated, the CUMULATIVE metric
|
||||
values MUST be reset and a new fixed start time following the last
|
||||
reported measurement time sent MUST be used.
|
||||
|
||||
For example, consider a system measuring the number of requests that
|
||||
it receives and reports the sum of these requests every second as a
|
||||
CUMULATIVE metric:
|
||||
|
||||
1. The system starts receiving at time=t_0.
|
||||
2. A request is received, the system measures 1 request.
|
||||
3. A request is received, the system measures 1 request.
|
||||
4. A request is received, the system measures 1 request.
|
||||
5. The 1 second collection cycle ends. A metric is exported for the
|
||||
number of requests received over the interval of time t_0 to
|
||||
t_0+1 with a value of 3.
|
||||
6. A request is received, the system measures 1 request.
|
||||
7. A request is received, the system measures 1 request.
|
||||
8. The 1 second collection cycle ends. A metric is exported for the
|
||||
number of requests received over the interval of time t_0 to
|
||||
t_0+2 with a value of 5.
|
||||
9. The system experiences a fault and loses state.
|
||||
10. The system recovers and resumes receiving at time=t_1.
|
||||
11. A request is received, the system measures 1 request.
|
||||
12. The 1 second collection cycle ends. A metric is exported for the
|
||||
number of requests received over the interval of time t_1 to
|
||||
t_1+1 with a value of 1.
|
||||
|
||||
Note: Even though, when reporting changes since last report time, using
|
||||
CUMULATIVE is valid, it is not recommended. */
|
||||
AGGREGATION_TEMPORALITY_CUMULATIVE = 2;
|
||||
}
|
||||
|
||||
// ValueType describes the type and units of a value, with an optional aggregation temporality.
|
||||
message ValueType {
|
||||
int32 type_strindex = 1; // Index into string table.
|
||||
int32 unit_strindex = 2; // Index into string table.
|
||||
|
||||
AggregationTemporality aggregation_temporality = 3;
|
||||
}
|
||||
|
||||
// Each Sample records values encountered in some program
|
||||
// context. The program context is typically a stack trace, perhaps
|
||||
// augmented with auxiliary information like the thread-id, some
|
||||
// indicator of a higher level request being handled etc.
|
||||
message Sample {
|
||||
// locations_start_index along with locations_length refers to to a slice of locations in Profile.location_indices.
|
||||
int32 locations_start_index = 1;
|
||||
// locations_length along with locations_start_index refers to a slice of locations in Profile.location_indices.
|
||||
// Supersedes location_index.
|
||||
int32 locations_length = 2;
|
||||
// The type and unit of each value is defined by the corresponding
|
||||
// entry in Profile.sample_type. All samples must have the same
|
||||
// number of values, the same as the length of Profile.sample_type.
|
||||
// When aggregating multiple samples into a single sample, the
|
||||
// result has a list of values that is the element-wise sum of the
|
||||
// lists of the originals.
|
||||
repeated int64 value = 3;
|
||||
// References to attributes in Profile.attribute_table. [optional]
|
||||
repeated int32 attribute_indices = 4;
|
||||
|
||||
// Reference to link in Profile.link_table. [optional]
|
||||
optional int32 link_index = 5;
|
||||
|
||||
// Timestamps associated with Sample represented in nanoseconds. These timestamps are expected
|
||||
// to fall within the Profile's time range. [optional]
|
||||
repeated uint64 timestamps_unix_nano = 6;
|
||||
}
|
||||
|
||||
// Describes the mapping of a binary in memory, including its address range,
|
||||
// file offset, and metadata like build ID
|
||||
message Mapping {
|
||||
// Address at which the binary (or DLL) is loaded into memory.
|
||||
uint64 memory_start = 1;
|
||||
// The limit of the address range occupied by this mapping.
|
||||
uint64 memory_limit = 2;
|
||||
// Offset in the binary that corresponds to the first mapped address.
|
||||
uint64 file_offset = 3;
|
||||
// The object this entry is loaded from. This can be a filename on
|
||||
// disk for the main binary and shared libraries, or virtual
|
||||
// abstractions like "[vdso]".
|
||||
int32 filename_strindex = 4; // Index into string table
|
||||
// References to attributes in Profile.attribute_table. [optional]
|
||||
repeated int32 attribute_indices = 5;
|
||||
// The following fields indicate the resolution of symbolic info.
|
||||
bool has_functions = 6;
|
||||
bool has_filenames = 7;
|
||||
bool has_line_numbers = 8;
|
||||
bool has_inline_frames = 9;
|
||||
}
|
||||
|
||||
// Describes function and line table debug information.
|
||||
message Location {
|
||||
// Reference to mapping in Profile.mapping_table.
|
||||
// It can be unset if the mapping is unknown or not applicable for
|
||||
// this profile type.
|
||||
optional int32 mapping_index = 1;
|
||||
// The instruction address for this location, if available. It
|
||||
// should be within [Mapping.memory_start...Mapping.memory_limit]
|
||||
// for the corresponding mapping. A non-leaf address may be in the
|
||||
// middle of a call instruction. It is up to display tools to find
|
||||
// the beginning of the instruction if necessary.
|
||||
uint64 address = 2;
|
||||
// Multiple line indicates this location has inlined functions,
|
||||
// where the last entry represents the caller into which the
|
||||
// preceding entries were inlined.
|
||||
//
|
||||
// E.g., if memcpy() is inlined into printf:
|
||||
// line[0].function_name == "memcpy"
|
||||
// line[1].function_name == "printf"
|
||||
repeated Line line = 3;
|
||||
// Provides an indication that multiple symbols map to this location's
|
||||
// address, for example due to identical code folding by the linker. In that
|
||||
// case the line information above represents one of the multiple
|
||||
// symbols. This field must be recomputed when the symbolization state of the
|
||||
// profile changes.
|
||||
bool is_folded = 4;
|
||||
|
||||
// References to attributes in Profile.attribute_table. [optional]
|
||||
repeated int32 attribute_indices = 5;
|
||||
}
|
||||
|
||||
// Details a specific line in a source code, linked to a function.
|
||||
message Line {
|
||||
// Reference to function in Profile.function_table.
|
||||
int32 function_index = 1;
|
||||
// Line number in source code.
|
||||
int64 line = 2;
|
||||
// Column number in source code.
|
||||
int64 column = 3;
|
||||
}
|
||||
|
||||
// Describes a function, including its human-readable name, system name,
|
||||
// source file, and starting line number in the source.
|
||||
message Function {
|
||||
// Name of the function, in human-readable form if available.
|
||||
int32 name_strindex = 1; // Index into string table
|
||||
// Name of the function, as identified by the system.
|
||||
// For instance, it can be a C++ mangled name.
|
||||
int32 system_name_strindex = 2; // Index into string table
|
||||
// Source file containing the function.
|
||||
int32 filename_strindex = 3; // Index into string table
|
||||
// Line number in source file.
|
||||
int64 start_line = 4;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2019, OpenTelemetry Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package opentelemetry.proto.resource.v1;
|
||||
|
||||
import "opentelemetry/proto/common/v1/common.proto";
|
||||
|
||||
option csharp_namespace = "OpenTelemetry.Proto.Resource.V1";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "io.opentelemetry.proto.resource.v1";
|
||||
option java_outer_classname = "ResourceProto";
|
||||
option go_package = "go.opentelemetry.io/proto/otlp/resource/v1";
|
||||
|
||||
// Resource information.
|
||||
message Resource {
|
||||
// Set of attributes that describe the resource.
|
||||
// Attribute keys MUST be unique (it is not allowed to have more than one
|
||||
// attribute with the same key).
|
||||
repeated opentelemetry.proto.common.v1.KeyValue attributes = 1;
|
||||
|
||||
// dropped_attributes_count is the number of dropped attributes. If the value is 0, then
|
||||
// no attributes were dropped.
|
||||
uint32 dropped_attributes_count = 2;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user