Repository: tavis-software/Tavis.UriTemplates Branch: main Commit: b8c26d0a2489 Files: 33 Total size: 111.4 KB Directory structure: gitextract_a58_z357/ ├── .gitattributes ├── .github/ │ ├── CODEOWNERS │ ├── dependabot.yml │ └── workflows/ │ ├── auto-merge-dependabot.yml │ ├── buildAndDeploy.yml │ └── codeql-analysis.yml ├── .gitignore ├── .gitmodules ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── UriTemplateKey.snk ├── UriTemplates.sln ├── build.cmd └── src/ ├── UriTemplateTests/ │ ├── App.config │ ├── ParameterMatchingTests.cs │ ├── SpecTests.cs │ ├── UriExtensionTests.cs │ ├── UriTemplateConverterTests.cs │ ├── UriTemplateExtensionsTests.cs │ ├── UriTemplateTableTests.cs │ ├── UriTemplateTests.csproj │ └── UsageTests.cs └── UriTemplates/ ├── OperatorInfo.cs ├── QueryStringParameterOrder.cs ├── Result.cs ├── UriTemplate.cs ├── UriTemplateConverter.cs ├── UriTemplateExtensions.cs ├── UriTemplateTable.cs ├── UriTemplates.csproj └── VarSpec.cs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ *.doc diff=astextplain *.DOC diff=astextplain *.docx diff=astextplain *.DOCX diff=astextplain *.dot diff=astextplain *.DOT diff=astextplain *.pdf diff=astextplain *.PDF diff=astextplain *.rtf diff=astextplain *.RTF diff=astextplain *.jpg binary *.png binary *.gif binary *.cs text diff=csharp *.vb text *.c text *.cpp text *.cxx text *.h text *.hxx text *.py text *.rb text *.java text *.html text *.htm text *.css text *.scss text *.sass text *.less text *.js text *.lisp text *.clj text *.sql text *.php text *.lua text *.m text *.asm text *.erl text *.fs text *.fsx text *.hs text *.csproj text merge=union *.vbproj text merge=union *.fsproj text merge=union *.dbproj text merge=union *.sln text eol=crlf merge=union ================================================ FILE: .github/CODEOWNERS ================================================ * @darrelmiller ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: nuget directory: "/" schedule: interval: daily open-pull-requests-limit: 10 - package-ecosystem: github-actions directory: "/" schedule: interval: daily open-pull-requests-limit: 10 - package-ecosystem: gitsubmodule directory: "/" schedule: interval: daily open-pull-requests-limit: 10 ================================================ FILE: .github/workflows/auto-merge-dependabot.yml ================================================ name: Auto-merge dependabot updates on: pull_request: branches: [ main ] permissions: pull-requests: write contents: write jobs: dependabot-merge: runs-on: ubuntu-latest if: ${{ github.actor == 'dependabot[bot]' }} steps: - name: Dependabot metadata id: metadata uses: dependabot/fetch-metadata@v2.4.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}" - name: Enable auto-merge for Dependabot PRs # Only if version bump is not a major version change if: ${{steps.metadata.outputs.update-type != 'version-update:semver-major'}} run: gh pr merge --auto --merge "$PR_URL" env: PR_URL: ${{github.event.pull_request.html_url}} GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} ================================================ FILE: .github/workflows/buildAndDeploy.yml ================================================ name: Build and Test on: workflow_dispatch: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest env: solutionName: UriTemplates.sln outputFolder: ./buildArtifacts steps: - name: Checkout repository uses: actions/checkout@v4 with: submodules: true - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: 6.0.x - name: Restore dependencies run: dotnet restore ${{ env.solutionName }} - name: Check formatting run: dotnet format --verify-no-changes --verbosity diagnostic - name: Build run: dotnet build ${{ env.solutionName }} --no-restore -c Release - name: Test run: dotnet test ${{ env.solutionName }} --no-build --verbosity normal -c Release /p:CollectCoverage=true /p:CoverletOutput=./TestResults/ /p:CoverletOutputFormat=opencover - name: Pack run: dotnet pack ${{ env.solutionName }} /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg --no-build --output ${{ env.outputFolder }} -c Release - name: Upload Nuget Package and Symbols uses: actions/upload-artifact@v4 with: name: drop path: | ${{ env.outputFolder }}/*.nupkg ${{ env.outputFolder }}/*.snupkg deploy: if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} environment: name: production runs-on: ubuntu-latest needs: [build] steps: - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: 6.0.x - name: Download artifacts uses: actions/download-artifact@v4 with: name: drop - name: Nuget push run: dotnet nuget push "*.nupkg" --skip-duplicate -s https://api.nuget.org/v3/index.json -k ${{ secrets.PUBLISH_GH_TOKEN }} ================================================ FILE: .github/workflows/codeql-analysis.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: workflow_dispatch: push: branches: [main] pull_request: # The branches below must be a subset of the branches above schedule: - cron: "20 9 * * 5" jobs: analyze: name: Analyze runs-on: ubuntu-latest strategy: fail-fast: false matrix: language: ["csharp"] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # Learn more: # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed steps: - name: Checkout repository uses: actions/checkout@v4 with: submodules: true - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: 6.0.x # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) # - name: Autobuild # uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language - name: Restore workloads run: | dotnet workload restore dotnet workload install wasm-tools - name: Restore dependencies run: dotnet restore - name: Build run: dotnet build --no-restore -c Release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 ================================================ FILE: .gitignore ================================================ #ignore thumbnails created by windows Thumbs.db #Ignore files build by Visual Studio *.obj *.exe *.pdb *.user *.aps *.pch *.vspscc *_i.c *_p.c *.ncb *.suo *.tlb *.tlh *.bak *.cache *.ilk *.log ClientBin [Bb]in [Dd]ebug*/ *.lib *.sbr obj/ [Rr]elease*/ _ReSharper*/ [Tt]est[Rr]esult* Examples*/ SerializerTest*/ *.csproj.user *.resharper* Download/ #Ignore files from MonoDevelop *.pidb *.userprefs packages artifacts *.nupkg publish-tavis*.ps1 project.lock.json .vs/ .idea ================================================ FILE: .gitmodules ================================================ [submodule "lib/uritemplate-test"] path = lib/uritemplate-test url = https://github.com/uri-templates/uritemplate-test.git ================================================ FILE: .travis.yml ================================================ language: csharp sudo: false # use the new container-based Travis infrastructure install: - nuget restore UriTemplates.sln script: - xbuild /p:Configuration=Release /t:Compile build/Build.proj ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ### Added - Added support for netstandard 2.1 [#69](https://github.com/tavis-software/Tavis.UriTemplates/issues/69) ### Changed ## [2.0.0] - [Breaking] Updated Target Framework Moniker to `netstandard2.0` and drops `net35`, `net40`, `net45` and `netstandard1.0`. ## [1.1.2] - Added Type converter support. ## [1.1.1] - Fixed bug parsing query parameter with comma delimited values ## [1.1.0] - Updated Target Framework Moniker from dotnet to netstandard1.0 ## [1.0.0] - This project has lived too long as a 0.x release. I believe it has seen enough production use to be considered a 1.0 release. ## [0.6.6] - Bugfix ## [0.6.5] - Added ability to retrieve URITemplates from UriTemplateTable - Parameter matching ## [0.6.4] - Added .net4 version of assembly - Updated nuget to put portable lib in dotnet folder to enable coreclr support - Made Resolve() thread safe by ensuring it does not share any state from one invocation to the next. - Added support for profile92 to allow including in Portable libraries that target .net4 - Added support for case insensitive parameter names. ## [0.6.3] - Added ToString() overload to allow retrieving unresolved template ## [0.6.2] - URI Template Extension AddParameters now uses IDictionary instead of Dictionary ## [0.6.1] - Added ClearParameter to unset a template parameter - Added MakeTemplate URI extension for creating a Uri template based on the query string parameters of a URI - Added GetQueryStringParameters URI extension for building dictionary of query parameters and values - Added AddParameters overload that accepts a dictionary of template parameters ## [0.6.0] - Added the ability to partially resolve templates using a new constructor parameter. - Added new fluent interface using extension methods for quickly creating a template and resolving it. - Created a .net 45 project - Restructured folders to comply with recommendations made by David Fowler - Added many more usage tests with real world scenarios - Added support for Windows 8.1 - Fixed Unicode encoding problem - Added support for passing non-string lists as parameters - Added support for passing parameters values that are non-string. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS Copyright 2012 Tavis Software Inc. 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. ================================================ FILE: README.md ================================================ # Uri Templates # [![Build and deploy](https://github.com/tavis-software/Tavis.UriTemplates/actions/workflows/buildAndDeploy.yml/badge.svg)](https://github.com/tavis-software/Tavis.UriTemplates/actions/workflows/buildAndDeploy.yml) [![CodeQL](https://github.com/tavis-software/Tavis.UriTemplates/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/tavis-software/Tavis.UriTemplates/actions/workflows/codeql-analysis.yml) [![NuGet](https://img.shields.io/nuget/v/Tavis.UriTemplates.svg?label=NuGet)](https://www.nuget.org/packages/Tavis.UriTemplates/) .NET implementation of the [URI Template Spec RFC6570](http://tools.ietf.org/html/rfc6570). Library implements Level 4 compliance and is tested against test cases from [UriTemplate test suite](https://github.com/uri-templates/uritemplate-test). Here are some basic usage examples: Replacing a path segment parameter, ```csharp [Fact] public void UpdatePathParameter() { var url = new UriTemplate("http://example.org/{tenant}/customers") .AddParameter("tenant", "acmé") .Resolve(); Assert.Equal("http://example.org/acm%C3%A9/customers", url); } ``` Setting query string parameters, ```csharp [Fact] public void ShouldResolveUriTemplateWithNonStringParameter() { var url = new UriTemplate("http://example.org/location{?lat,lng}") .AddParameters(new { lat = 31.464, lng = 74.386 }) .Resolve(); Assert.Equal("http://example.org/location?lat=31.464&lng=74.386", url); } ``` Resolving a URI when parameters are not set will simply remove the parameters, ```csharp [Fact] public void SomeParametersFromAnObject() { var url = new UriTemplate("http://example.org{/environment}{/version}/customers{?active,country}") .AddParameters(new { version = "v2", active = "true" }) .Resolve(); Assert.Equal("http://example.org/v2/customers?active=true", url); } ``` You can even pass lists as parameters ```csharp [Fact] public void ApplyParametersObjectWithAListofInts() { var url = new UriTemplate("http://example.org/customers{?ids,order}") .AddParameters(new { order = "up", ids = new[] {21, 75, 21} }) .Resolve(); Assert.Equal("http://example.org/customers?ids=21,75,21&order=up", url); } ``` And dictionaries, ```csharp [Fact] public void ApplyDictionaryToQueryParameters() { var url = new UriTemplate("http://example.org/foo{?coords*}") .AddParameter("coords", new Dictionary { {"x", "1"}, {"y", "2"}, }) .Resolve(); Assert.Equal("http://example.org/foo?x=1&y=2", url); } ``` We also handle all the complex URI encoding rules automatically. ```csharp [Fact] public void TestExtremeEncoding() { var url = new UriTemplate("http://example.org/sparql{?query}") .AddParameter("query", "PREFIX dc: SELECT ?book ?who WHERE { ?book dc:creator ?who }") .Resolve(); Assert.Equal("http://example.org/sparql?query=PREFIX%20dc%3A%20%3Chttp%3A%2F%2Fpurl.org%2Fdc%2Felements%2F1.1%2F%3E%20SELECT%20%3Fbook%20%3Fwho%20WHERE%20%7B%20%3Fbook%20dc%3Acreator%20%3Fwho%20%7D", url); } ``` There is a [blogpost](http://bizcoder.com/constructing-urls-the-easy-way) that discusses these examples and more in detail. As well as having a set of regular usage tests, this library also executes tests based on a standard test suite. This test suite is pulled in as a Git Submodule, therefore when cloning this repo, you will need use the `--recursive` switch, git clone --recursive git@github.com:tavis-software/Tavis.UriTemplates.git Current this library does not pass all of the failure tests. I.e. If you pass an invalid URI Template, you may not get an exception. ================================================ FILE: UriTemplates.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.28307.489 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0C994FD2-43CE-4412-BCCD-628DBD8130DF}" ProjectSection(SolutionItems) = preProject License.txt = License.txt Readme.md = Readme.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{718037C6-0F82-456A-B7EB-531694D035B4}" ProjectSection(SolutionItems) = preProject ReleaseNotes.md = ReleaseNotes.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UriTemplates", "src\UriTemplates\UriTemplates.csproj", "{F60E3FBC-22FE-43A7-A46B-93C30B74B72A}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UriTemplateTests", "src\UriTemplateTests\UriTemplateTests.csproj", "{2D43074E-C8E2-4D1D-B64D-7A267908840B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|Mixed Platforms = Debug|Mixed Platforms Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU Release|Mixed Platforms = Release|Mixed Platforms Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {F60E3FBC-22FE-43A7-A46B-93C30B74B72A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F60E3FBC-22FE-43A7-A46B-93C30B74B72A}.Debug|Any CPU.Build.0 = Debug|Any CPU {F60E3FBC-22FE-43A7-A46B-93C30B74B72A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {F60E3FBC-22FE-43A7-A46B-93C30B74B72A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {F60E3FBC-22FE-43A7-A46B-93C30B74B72A}.Debug|x86.ActiveCfg = Debug|Any CPU {F60E3FBC-22FE-43A7-A46B-93C30B74B72A}.Debug|x86.Build.0 = Debug|Any CPU {F60E3FBC-22FE-43A7-A46B-93C30B74B72A}.Release|Any CPU.ActiveCfg = Release|Any CPU {F60E3FBC-22FE-43A7-A46B-93C30B74B72A}.Release|Any CPU.Build.0 = Release|Any CPU {F60E3FBC-22FE-43A7-A46B-93C30B74B72A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {F60E3FBC-22FE-43A7-A46B-93C30B74B72A}.Release|Mixed Platforms.Build.0 = Release|Any CPU {F60E3FBC-22FE-43A7-A46B-93C30B74B72A}.Release|x86.ActiveCfg = Release|Any CPU {F60E3FBC-22FE-43A7-A46B-93C30B74B72A}.Release|x86.Build.0 = Release|Any CPU {2D43074E-C8E2-4D1D-B64D-7A267908840B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2D43074E-C8E2-4D1D-B64D-7A267908840B}.Debug|Any CPU.Build.0 = Debug|Any CPU {2D43074E-C8E2-4D1D-B64D-7A267908840B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {2D43074E-C8E2-4D1D-B64D-7A267908840B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {2D43074E-C8E2-4D1D-B64D-7A267908840B}.Debug|x86.ActiveCfg = Debug|Any CPU {2D43074E-C8E2-4D1D-B64D-7A267908840B}.Debug|x86.Build.0 = Debug|Any CPU {2D43074E-C8E2-4D1D-B64D-7A267908840B}.Release|Any CPU.ActiveCfg = Release|Any CPU {2D43074E-C8E2-4D1D-B64D-7A267908840B}.Release|Any CPU.Build.0 = Release|Any CPU {2D43074E-C8E2-4D1D-B64D-7A267908840B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {2D43074E-C8E2-4D1D-B64D-7A267908840B}.Release|Mixed Platforms.Build.0 = Release|Any CPU {2D43074E-C8E2-4D1D-B64D-7A267908840B}.Release|x86.ActiveCfg = Release|Any CPU {2D43074E-C8E2-4D1D-B64D-7A267908840B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C814C2F5-05C7-4FAD-A250-2EB1FC0364A0} EndGlobalSection EndGlobal ================================================ FILE: build.cmd ================================================ @echo Off set config=%1 if "%config%" == "" ( set config=debug ) md artifacts dotnet build --configuration %config% --verbosity normal /m /v:M /fl /flp:LogFile=msbuild.log; /nr:false dotnet pack --no-build --configuration %config% --output %cd%\artifacts ================================================ FILE: src/UriTemplateTests/App.config ================================================  ================================================ FILE: src/UriTemplateTests/ParameterMatchingTests.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using Tavis.UriTemplates; using Xunit; using Xunit.Extensions; namespace UriTemplateTests { public class ParameterMatchingTests { [Fact] public void MatchUriToTemplate() { var uri = new Uri("http://example.com/foo/bar"); var sTemplate = "http://example.com/{p1}/{p2}"; var x = UriTemplate.CreateMatchingRegex(sTemplate); var match = Regex.IsMatch(uri.AbsoluteUri, x); Assert.True(match); } [Fact] public void GetParameters() { var uri = new Uri("http://example.com/foo/bar"); var sTemplate = "http://example.com/{p1}/{p2}"; var x = UriTemplate.CreateMatchingRegex(sTemplate); var regex = new Regex(x); var match = regex.Match(uri.AbsoluteUri); Assert.Equal("foo", match.Groups["p1"].Value); Assert.Equal("bar", match.Groups["p2"].Value); } [Fact] public void GetParametersWithOperators() { var uri = new Uri("http://example.com/foo/bar"); var template = new UriTemplate("http://example.com/{+p1}/{p2*}"); var parameters = template.GetParameters(uri); Assert.Equal(2, parameters.Count); Assert.Equal("foo", parameters["p1"]); Assert.Equal("bar", parameters["p2"]); } [Fact] public void GetParametersFromQueryString() { var uri = new Uri("http://example.com/foo/bar?blur=45"); var template = new UriTemplate("http://example.com/{+p1}/{p2*}{?blur}"); var parameters = template.GetParameters(uri); Assert.Equal(3, parameters.Count); Assert.Equal("foo", parameters["p1"]); Assert.Equal("bar", parameters["p2"]); Assert.Equal("45", parameters["blur"]); } [Fact] public void GetParametersFromMultipleQueryString() { var uri = new Uri("http://example.com/foo/bar?blur=45"); var template = new UriTemplate("http://example.com/{+p1}/{p2*}{?blur,blob}"); var parameters = template.GetParameters(uri); Assert.Equal(3, parameters.Count); Assert.Equal("foo", parameters["p1"]); Assert.Equal("bar", parameters["p2"]); Assert.Equal("45", parameters["blur"]); } [Fact] public void GetParametersFromMultipleQueryStringWithTwoParamValues() { var uri = new Uri("http://example.com/foo/bar?blur=45&blob=23"); var template = new UriTemplate("http://example.com/{+p1}/{p2*}{?blur,blob}"); var parameters = template.GetParameters(uri); Assert.Equal(4, parameters.Count); Assert.Equal("foo", parameters["p1"]); Assert.Equal("bar", parameters["p2"]); Assert.Equal("45", parameters["blur"]); Assert.Equal("23", parameters["blob"]); } [Fact] public void GetParameterFromArrayParameter() { var uri = new Uri("http://example.com?blur=45,23"); var template = new UriTemplate("http://example.com{?blur}"); var parameters = template.GetParameters(uri); Assert.Single(parameters); Assert.Equal("45,23", parameters["blur"]); } [Fact] public void GetParametersFromMultipleQueryStringWithOptionalAndMandatoryParameters() { var uri = new Uri("http://example.com/foo/bar?blur=45&blob=23"); var template = new UriTemplate("http://example.com/{+p1}/{p2*}{?blur}{&blob}"); var parameters = template.GetParameters(uri); Assert.Equal(4, parameters.Count); Assert.Equal("foo", parameters["p1"]); Assert.Equal("bar", parameters["p2"]); Assert.Equal("45", parameters["blur"]); Assert.Equal("23", parameters["blob"]); } [Fact] public void GetParametersFromMultipleQueryStringWithOptionalParameters() { var uri = new Uri("http://example.com/foo/bar"); var template = new UriTemplate("http://example.com/{+p1}/{p2*}{?blur,blob}"); var parameters = template.GetParameters(uri); Assert.Equal("foo", parameters["p1"]); Assert.Equal("bar", parameters["p2"]); } [Fact] public void TestGlimpseUrl() { var uri = new Uri("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId=123232323&hash=23ADE34FAE&callback=http%3A%2F%2Fexample.com%2Fcallback"); var template = new UriTemplate("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId={parentRequestId}{&hash,callback}"); var parameters = template.GetParameters(uri); Assert.Equal(3, parameters.Count); Assert.Equal("123232323", parameters["parentRequestId"]); Assert.Equal("23ADE34FAE", parameters["hash"]); Assert.Equal("http://example.com/callback", parameters["callback"]); } [Fact] public void TestUrlWithQuestionMarkAsFirstCharacter() { var parameters = new UriTemplate("?hash={hash}").GetParameters(new Uri("http://localhost:5000/glimpse/metadata?hash=123")); ; Assert.Single(parameters); Assert.Equal("123", parameters["hash"]); } [Fact] public void TestExactParameterCount() { var uri = new Uri("http://example.com/foo?bar=10"); var template = new UriTemplate("http://example.com/foo{?bar}"); var parameters = template.GetParameters(uri); Assert.Single(parameters); } [Fact] public void SimplePerfTest() { var uri = new Uri("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId=123232323&hash=23ADE34FAE&callback=http%3A%2F%2Fexample.com%2Fcallback"); var template = new UriTemplate("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId={parentRequestId}{&hash,callback}"); for (int i = 0; i < 100000; i++) { var parameters = template.GetParameters(uri); } } [Fact] public void Level1Decode() { var uri = new Uri("/Hello%20World", UriKind.RelativeOrAbsolute); var template = new UriTemplate("/{p1}"); var parameters = template.GetParameters(uri); Assert.Equal("Hello World", parameters["p1"]); } //[Fact] //public void Level2Decode() //{ // var uri = new Uri("/foo?path=Hello/World", UriKind.RelativeOrAbsolute); // var template = new UriTemplate("/foo?path={+p1}"); // var parameters = template.GetParameters(uri); // Assert.Equal("Hello/World", parameters["p1"]); //} [Fact] public void FragmentParam() { var uri = new Uri("/foo#Hello%20World!", UriKind.RelativeOrAbsolute); var template = new UriTemplate("/foo{#p1}"); var parameters = template.GetParameters(uri); Assert.Equal("Hello World!", parameters["p1"]); } [Fact] public void FragmentParams() { var uri = new Uri("/foo#Hello%20World!,blurg", UriKind.RelativeOrAbsolute); var template = new UriTemplate("/foo{#p1,p2}"); var parameters = template.GetParameters(uri); Assert.Equal("Hello World!", parameters["p1"]); Assert.Equal("blurg", parameters["p2"]); } [Fact] public void OptionalPathParam() { var uri = new Uri("/foo/yuck/bob", UriKind.RelativeOrAbsolute); var template = new UriTemplate("/foo{/bar}/bob"); var parameters = template.GetParameters(uri); Assert.Equal("yuck", parameters["bar"]); } [Fact] public void OptionalPathParamWithMultipleValues() { var uri = new Uri("/foo/yuck/yob/bob", UriKind.RelativeOrAbsolute); var template = new UriTemplate("/foo{/bar,baz}/bob"); var parameters = template.GetParameters(uri); Assert.Equal(2, parameters.Count); // This current fails Assert.Equal("yuck", parameters["bar"]); Assert.Equal("yob", parameters["baz"]); } } } ================================================ FILE: src/UriTemplateTests/SpecTests.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Linq; using Newtonsoft.Json.Linq; using Tavis.UriTemplates; using Xunit; using Xunit.Extensions; namespace UriTemplateTests { public class UriTemplateTests2 { [Theory, MemberData(nameof(SpecSamples))] public void SpecSamplesTest(string template, string[] results, TestSet.TestCase testCase) { var uriTemplate = new UriTemplate(template); foreach (var variable in testCase.TestSet.Variables) { uriTemplate.SetParameter(variable.Key, variable.Value); } string result = null; result = uriTemplate.Resolve(); Assert.Contains(results, x => x == result); } [Theory, MemberData(nameof(ExtendedSamples))] public void ExtendedSamplesTest(string template, string[] results, TestSet.TestCase testCase) { var uriTemplate = new UriTemplate(template); foreach (var variable in testCase.TestSet.Variables) { uriTemplate.SetParameter(variable.Key, variable.Value); } string result = null; ArgumentException aex = null; try { result = uriTemplate.Resolve(); } catch (ArgumentException ex) { aex = ex; } if (results[0] == "False") { Assert.NotNull(aex); } else { Assert.Contains(results, x => x == result); } } [Theory(Skip = "Disabled for the moment."), MemberData(nameof(FailureSamples))] public void FailureSamplesTest(string template, string[] results, TestSet.TestCase testCase) { var uriTemplate = new UriTemplate(template); foreach (var variable in testCase.TestSet.Variables) { uriTemplate.SetParameter(variable.Key, variable.Value); } string result = null; ArgumentException aex = null; try { result = uriTemplate.Resolve(); } catch (ArgumentException ex) { aex = ex; } if (results[0] == "False") { Assert.NotNull(aex); } else { Assert.Contains(results, x => x == result); } } public static IEnumerable SpecSamples { get { Stream stream = null; var suites = new List>(); stream = typeof(UriTemplateTests2).Assembly.GetManifestResourceStream("UriTemplateTests.spec-examples.json"); suites.Add(CreateTestSuite(new StreamReader(stream).ReadToEnd())); stream = typeof(UriTemplateTests2).Assembly.GetManifestResourceStream("UriTemplateTests.spec-examples-by-section.json"); suites.Add(CreateTestSuite(new StreamReader(stream).ReadToEnd())); foreach (var suite in suites) { foreach (var testset in suite.Values) { foreach (var testCase in testset.TestCases) { yield return new object[] { testCase.Template, testCase.Result, testCase }; } } } } } public static IEnumerable ExtendedSamples { get { Stream stream = null; var suites = new List>(); stream = typeof(UriTemplateTests2).Assembly.GetManifestResourceStream("UriTemplateTests.extended-tests.json"); suites.Add(CreateTestSuite(new StreamReader(stream).ReadToEnd())); foreach (var suite in suites) { foreach (var testset in suite.Values) { foreach (var testCase in testset.TestCases) { yield return new object[] { testCase.Template, testCase.Result, testCase }; } } } } } public static IEnumerable FailureSamples { get { Stream stream = null; var suites = new List>(); stream = typeof(UriTemplateTests2).Assembly.GetManifestResourceStream("UriTemplateTests.negative-tests.json"); suites.Add(CreateTestSuite(new StreamReader(stream).ReadToEnd())); foreach (var suite in suites) { foreach (var testset in suite.Values) { foreach (var testCase in testset.TestCases) { yield return new object[] { testCase.Template, testCase.Result, testCase }; } } } } } private static Dictionary CreateTestSuite(string json) { JObject token = JObject.Parse(json); var testSuite = new Dictionary(); foreach (JProperty levelSet in token.Children()) { testSuite.Add(levelSet.Name, CreateTestSet(levelSet.Name, levelSet.Value)); } return testSuite; } private static TestSet CreateTestSet(string name, JToken token) { var testSet = new TestSet(); testSet.Name = name; var variables = token["variables"]; foreach (JProperty variable in variables) { ParseVariable(variable, testSet.Variables); } var testcases = token["testcases"]; foreach (var testcase in testcases) { testSet.TestCases.Add(CreateTestCase(testSet, testcase)); } return testSet; } private static void ParseVariable(JProperty variable, Dictionary dictionary) { if (variable.Value.Type == JTokenType.Array) { var array = (JArray)variable.Value; if (array.Count == 0) { dictionary.Add(variable.Name, new List()); } else { dictionary.Add(variable.Name, array.Values()); } } else if (variable.Value.Type == JTokenType.Object) { var jvalue = (JObject)variable.Value; var dict = new Dictionary(); foreach (var prop in jvalue.Properties()) { dict[prop.Name] = prop.Value.ToString(); } dictionary.Add(variable.Name, dict); } else { if (((JValue)variable.Value).Value == null) { dictionary.Add(variable.Name, null); } else { dictionary.Add(variable.Name, variable.Value.ToString()); } } } private static TestSet.TestCase CreateTestCase(TestSet testSet, JToken testcase) { var testCase = new TestSet.TestCase(testSet); testCase.Template = testcase[0].Value(); if (testcase[1].Type == JTokenType.Array) { var results = (JArray)testcase[1]; testCase.Result = results.Select(jv => jv.Value()).ToArray(); } else { testCase.Result = new string[1]; testCase.Result[0] = testcase[1].Value(); } return testCase; } public class TestSet { public string Name { get; set; } public int level { get; set; } public Dictionary Variables = new Dictionary(); public List TestCases = new List(); public class TestCase { private readonly TestSet _testSet; public TestCase(TestSet testSet) { _testSet = testSet; } public TestSet TestSet { get { return _testSet; } } public string Template { get; set; } public string[] Result { get; set; } } } } } ================================================ FILE: src/UriTemplateTests/UriExtensionTests.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using Tavis.UriTemplates; using Xunit; namespace UriTemplateTests { public class UriExtensionTests { [Fact] public void Change_an_existing_parameter_within_multiple() { var target = new Uri("http://example/customer?view=False&foo=bar"); var parameters = target.GetQueryStringParameters(); parameters["view"] = true; var template = target.MakeTemplate(parameters); Assert.Equal("http://example/customer?view=True&foo=bar", template.Resolve()); } [Fact] public void Change_an_existing_parameter() { var target = new Uri("http://example/customer?view=False&foo=bar"); var template = target.MakeTemplate(); template.SetParameter("view", true); Assert.Equal("http://example/customer?view=True&foo=bar", template.Resolve()); } [Fact] public void Remove_an_existing_parameter() { var target = new Uri("http://example/customer?view=False&foo=bar"); var template = target.MakeTemplate(); template.ClearParameter("view"); Assert.Equal("http://example/customer?foo=bar", template.Resolve()); } [Fact] public void Remove_a_query_parameters2() { var target = new Uri("http://example.org/customer?format=xml&id=23"); var template = target.MakeTemplate(); template.ClearParameter("format"); Assert.Equal("http://example.org/customer?id=23", template.Resolve()); } [Fact] public void Add_multiple_parameters_to_uri() { var target = new Uri("http://example/customer"); var template = target.MakeTemplate(new Dictionary { {"id", 99}, {"view", false} }); Assert.Equal("http://example/customer?id=99&view=False", template.Resolve()); } [Fact] public void Add_parameters_to_uri_with_query_string_ignoring_path_parameter() { var target = new Uri("http://example/customer/{id}?view=true"); var template = target.MakeTemplate(target.GetQueryStringParameters() .Union(new Dictionary { { "context", "detail" } }) .ToDictionary(k => k.Key, v => v.Value)); template.AddParameter("id", 99); Assert.Equal("http://example/customer/99?view=true&context=detail", template.Resolve()); } } } ================================================ FILE: src/UriTemplateTests/UriTemplateConverterTests.cs ================================================ using System.ComponentModel; using Tavis.UriTemplates; using Xunit; namespace UriTemplateTests { public class UriTemplateConverterTests { [Theory] [InlineData("http://example.org/{tenant}/customers")] [InlineData("http://example.org/{environment}/{version}/customers{?active,country}")] [InlineData("http://example.org/foo{?coords*}")] public void ConvertFromString(string rawTemplate) { var converter = TypeDescriptor.GetConverter(typeof(UriTemplate)); var template = converter.ConvertFromString(rawTemplate); Assert.NotNull(template); Assert.Equal(rawTemplate, template.ToString()); } } } ================================================ FILE: src/UriTemplateTests/UriTemplateExtensionsTests.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using Tavis; using Tavis.UriTemplates; using Xunit; namespace UriTemplateTests { public class UriTemplateExtensionsTests { [Fact] public void UpdatePathParameter() { var url = new UriTemplate("http://example.org/{tenant}/customers") .AddParameter("tenant", "acmé") .Resolve(); Assert.Equal("http://example.org/acm%C3%A9/customers", url); } [Fact] public void QueryParametersTheOldWay() { var url = new UriTemplate("http://example.org/customers?active={activeflag}") .AddParameter("activeflag", "true") .Resolve(); Assert.Equal("http://example.org/customers?active=true", url); } [Fact] public void QueryParametersTheNewWay() { var url = new UriTemplate("http://example.org/customers{?active}") .AddParameter("active", "true") .Resolve(); Assert.Equal("http://example.org/customers?active=true", url); } [Fact] public void QueryParametersTheNewWayWithoutValue() { var url = new UriTemplate("http://example.org/customers{?active}") .AddParameters(null) .Resolve(); Assert.Equal("http://example.org/customers", url); } [Fact] public void ShouldResolveUriTemplateWithNonStringParameter() { var url = new UriTemplate("http://example.org/location{?lat,lng}") .AddParameters(new { lat = 31.464, lng = 74.386 }) .Resolve(); Assert.Equal("http://example.org/location?lat=31.464&lng=74.386", url); } [Fact] public void ParametersFromAnObject() { var url = new UriTemplate("http://example.org/{environment}/{version}/customers{?active,country}") .AddParameters(new { environment = "dev", version = "v2", active = "true", country = "CA" }) .Resolve(); Assert.Equal("http://example.org/dev/v2/customers?active=true&country=CA", url); } [Fact] public void SomeParametersFromAnObject() { var url = new UriTemplate("http://example.org{/environment}{/version}/customers{?active,country}") .AddParameters(new { version = "v2", active = "true" }) .Resolve(); Assert.Equal("http://example.org/v2/customers?active=true", url); } [Fact] public void ApplyDictionaryToQueryParameters() { var url = new UriTemplate("http://example.org/foo{?coords*}") .AddParameter("coords", new Dictionary { {"x", "1"}, {"y", "2"}, }) .Resolve(); Assert.Equal("http://example.org/foo?x=1&y=2", url); } [Fact] public void ApplyParametersObjectToPathSegment() { var url = new UriTemplate("http://example.org/foo/{bar}/baz") .AddParameters(new { bar = "yo" }) .Resolve(); Assert.Equal("http://example.org/foo/yo/baz", url); } [Fact] public void ExtremeEncoding() { var url = new UriTemplate("http://example.org/sparql{?query}") .AddParameter("query", "PREFIX dc: SELECT ?book ?who WHERE { ?book dc:creator ?who }") .Resolve(); Assert.Equal("http://example.org/sparql?query=PREFIX%20dc%3A%20%3Chttp%3A%2F%2Fpurl.org%2Fdc%2Felements%2F1.1%2F%3E%20SELECT%20%3Fbook%20%3Fwho%20WHERE%20%7B%20%3Fbook%20dc%3Acreator%20%3Fwho%20%7D", url); } [Fact] public void ApplyParametersObjectWithAList() { var url = new UriTemplate("http://example.org/customers{?ids,order}") .AddParameters(new { order = "up", ids = new List { "21", "75", "21" } }).Resolve(); Assert.Equal("http://example.org/customers?ids=21,75,21&order=up", url); } [Fact] public void ApplyParametersObjectWithAListofInts() { var url = new UriTemplate("http://example.org/customers{?ids,order}") .AddParameters(new { order = "up", ids = new[] { 21, 75, 21 } }) .Resolve(); Assert.Equal("http://example.org/customers?ids=21,75,21&order=up", url); } [Fact] public void ApplyParametersObjectWithAListofIntsExploded() { var url = new UriTemplate("http://example.org/customers{?ids*,order}") .AddParameters(new { order = "up", ids = new[] { 21, 75, 21 } }) .Resolve(); Assert.Equal("http://example.org/customers?ids=21&ids=75&ids=21&order=up", url); } [Fact] public void ApplyFoldersToPath() { var url = new UriTemplate("http://example.org/files{/folders*}{?filename}") .AddParameters(new { folders = new[] { "customer", "project" }, filename = "proposal.pdf" }) .Resolve(); Assert.Equal("http://example.org/files/customer/project?filename=proposal.pdf", url); } [Fact] public void ParametersFromAnObjectFromInvalidUrl() { var url = new UriTemplate("http://{environment}.example.org/{version}/customers{?active,country}") .AddParameters(new { environment = "dev", version = "v2", active = "true", country = "CA" }) .Resolve(); Assert.Equal("http://dev.example.org/v2/customers?active=true&country=CA", url); } [Fact] public void ApplyFoldersToPathFromStringNotUrl() { var url = new UriTemplate("http://example.org{/folders*}{?filename}") .AddParameters(new { folders = new[] { "files", "customer", "project" }, filename = "proposal.pdf" }) .Resolve(); Assert.Equal("http://example.org/files/customer/project?filename=proposal.pdf", url); } [Fact] public void ReplaceBaseAddress() { var url = new UriTemplate("{+baseUrl}api/customer/{id}") .AddParameters(new { baseUrl = "http://example.org/", id = "22" }) .Resolve(); Assert.Equal("http://example.org/api/customer/22", url); } [Fact] public void ReplaceBaseAddressButNotId() { var url = new UriTemplate("{+baseUrl}api/customer/{id}", resolvePartially: true) .AddParameters(new { baseUrl = "http://example.org/" }) .Resolve(); Assert.Equal("http://example.org/api/customer/{id}", url); } [Fact] public void PartiallyParametersFromAnObjectFromInvalidUrl() { var url = new UriTemplate("http://{environment}.example.org/{version}/customers{?active,country}", resolvePartially: true) .AddParameters(new { environment = "dev", version = "v2" }) .Resolve(); Assert.Equal("http://dev.example.org/v2/customers{?active,country}", url); } [Fact] public void PartiallyApplyFoldersToPathFromStringNotUrl() { var url = new UriTemplate("http://example.org{/folders*}{?filename}", true) .AddParameters(new { filename = "proposal.pdf" }) .Resolve(); Assert.Equal("http://example.org{/folders*}?filename=proposal.pdf", url); } [Fact] public void UseArbitraryClassAsParameter() { var url = new UriTemplate("/{test}", true) .AddParameters(new { test = new Something() }) .Resolve(); Assert.Equal("/something", url); } [Fact] public void AddMultipleParametersToLink() { var template = new UriTemplate("http://localhost/api/{dataset}/customer{?foo,bar,baz}"); template.AddParameters(new Dictionary { {"foo", "bar"}, {"baz", "99"}, {"dataset", "bob"} }); var uri = template.Resolve(); Assert.Equal("http://localhost/api/bob/customer?foo=bar&baz=99", uri); } } class Something { public override string ToString() { return "something"; } } } ================================================ FILE: src/UriTemplateTests/UriTemplateTableTests.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using Tavis.UriTemplates; using Xunit; namespace UriTemplateTests { public class UriTemplateTableTests { [Theory, InlineData("/", "root"), InlineData("/baz/fod/burg", ""), InlineData("/baz/kit", "kit"), InlineData("/baz/fod", "baz"), InlineData("/baz/fod/blob", "blob"), InlineData("/glah/flid/blob", "goo")] public void FindPathTemplates(string url, string key) { var table = new UriTemplateTable(); // Shorter paths and literal path segments should be added to the table first. table.Add("root", new UriTemplate("/")); table.Add("foo", new UriTemplate("/foo/{bar}")); table.Add("kit", new UriTemplate("/baz/kit")); table.Add("baz", new UriTemplate("/baz/{bar}")); table.Add("blob", new UriTemplate("/baz/{bar}/blob")); table.Add("goo", new UriTemplate("/{goo}/{bar}/blob")); var result = table.Match(new Uri(url, UriKind.RelativeOrAbsolute)); if (string.IsNullOrEmpty(key)) { Assert.Null(result); } else { Assert.Equal(key, result.Key); } Assert.NotNull(table["goo"]); Assert.Null(table["goo1"]); } [Theory, InlineData("/games", "games"), InlineData("/games/monopoly/Setup/23", "gamessetup"), InlineData("/games/monopoly/Resources/foo/23", "resource"), InlineData("/games/monopoly/22/Chat/33", "chat"), InlineData("/games/monopoly/22/State/33", "state"), ] public void FindTemplatesInGamesApi(string url, string key) { var table = new UriTemplateTable(); table.Add("games", new UriTemplate("/games")); table.Add("gamessetup", new UriTemplate("/games/{gametitle}/Setup/{gamesid}")); table.Add("resource", new UriTemplate("/games/{gametitle}/Resources/{resourcetype}/{resourceid}")); table.Add("chat", new UriTemplate("/games/{gametitle}/{gameid}/Chat/{chatid}")); table.Add("state", new UriTemplate("/games/{gametitle}/{gameid}/State/{stateid}")); var result = table.Match(new Uri(url, UriKind.RelativeOrAbsolute)); if (string.IsNullOrEmpty(key)) { Assert.Null(result); } else { Assert.Equal(key, result.Key); } } [Theory, InlineData("/foo?x=1&y=2", "fooxy3"), InlineData("/foo?x=1", "fooxy2"), InlineData("/foo?x=a,b,c,d", "fooxy2"), InlineData("/foo?y=2", "fooxy"), InlineData("/foo", "fooxy"), ] public void FindTemplatesWithQueryStrings(string url, string key) { var table = new UriTemplateTable(); // More restrictive templates should have priority over less restrictive ones table.Add("fooxy3", new UriTemplate("/foo?x={x}&y={y}")); table.Add("fooxy2", new UriTemplate("/foo?x={x}{&y}")); table.Add("fooxy4", new UriTemplate("/foo?x={x}{&z}")); table.Add("fooxy", new UriTemplate("/foo{?x,y}")); table.Add("foo", new UriTemplate("/foo")); var result = table.Match(new Uri(url, UriKind.RelativeOrAbsolute)); if (string.IsNullOrEmpty(key)) { Assert.Null(result); } else { Assert.Equal(key, result.Key); } } [Fact] public void FindTemplatesWithArrayQueryParameters() { var table = new UriTemplateTable(); // More restrictive templates should have priority over less restrictive ones table.Add("fooxy3", new UriTemplate("/foo?x={x}&y={y}")); table.Add("fooxy2", new UriTemplate("/foo?x={x}{&y}")); table.Add("fooxy4", new UriTemplate("/foo?x={x}{&z}")); table.Add("fooxy", new UriTemplate("/foo{?x,y}")); table.Add("foo", new UriTemplate("/foo")); var result = table.Match(new Uri("/foo?x=a,b,c,d", UriKind.RelativeOrAbsolute)); Assert.Equal("fooxy2", result.Key); } [Fact] public void MatchTemplateWithDifferentOrderOfQueryStringParameters() { var table = new UriTemplateTable(); // More restrictive templates should have priority over less restrictive ones table.Add("fooxy3", new UriTemplate("/foo?x={x}&y={y}")); var result = table.Match(new Uri("/foo?y=a&x=b", UriKind.RelativeOrAbsolute), QueryStringParameterOrder.Any); Assert.Equal("fooxy3", result.Key); } } } ================================================ FILE: src/UriTemplateTests/UriTemplateTests.csproj ================================================ net6.0 false ================================================ FILE: src/UriTemplateTests/UsageTests.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Text; using Tavis.UriTemplates; using Xunit; namespace UriTemplateTests { public class UsageTests { [Fact] public void TestHexEscape() { for (int i = 20; i < 128; i++) { Assert.Equal(Uri.HexEscape((char)i), Result.HexEscape((char)i)); } } [Fact] public void ShouldAllowUriTemplateWithPathSegmentParameter() { var template = new UriTemplate("http://example.org/foo/{bar}/baz"); template.SetParameter("bar", "yo"); var uriString = template.Resolve(); Assert.Equal("http://example.org/foo/yo/baz", uriString); } [Fact] public void ShouldAllowUriTemplateWithMultiplePathSegmentParameter() { var template = new UriTemplate("http://example.org/foo/{bar}/baz/{blar}"); template.SetParameter("bar", "yo"); template.SetParameter("blar", "yuck"); var uriString = template.Resolve(); Assert.Equal("http://example.org/foo/yo/baz/yuck", uriString); } [Fact] public void ShouldResolveUriTemplateWithNonStringParameter() { var template = new UriTemplate("http://example.org/foo/{bar}/baz{?lat,lng}"); double lat = 31.464, lng = 74.386; template.SetParameter("bar", "yo"); template.SetParameter("lat", lat); template.SetParameter("lng", lng); var uriString = template.Resolve(); Assert.Equal("http://example.org/foo/yo/baz?lat=31.464&lng=74.386", uriString); } [Fact] public void ShouldResolveMatrixParameter() { var template = new UriTemplate("http://example.org/foo{;lat,lng}"); double lat = 31.464, lng = 74.386; template.SetParameter("lat", lat); template.SetParameter("lng", lng); var uriString = template.Resolve(); Assert.Equal("http://example.org/foo;lat=31.464;lng=74.386", uriString); } [Fact] public void ShouldAllowUriTemplateWithQueryParamsButNoValues() { var template = new UriTemplate("http://example.org/foo{?bar,baz}"); //template.SetParameter("bar", "yo"); //template.SetParameter("blar", "yuck"); var uriString = template.Resolve(); Assert.Equal("http://example.org/foo", uriString); } [Fact] public void ShouldAllowPartialUriTemplateWithQueryParamsButNoValues() { var template = new UriTemplate("http://example.org/foo{?bar,baz}", resolvePartially: true); //template.SetParameter("bar", "yo"); //template.SetParameter("blar", "yuck"); var uriString = template.Resolve(); Assert.Equal("http://example.org/foo{?bar,baz}", uriString); } [Fact] public void ShouldAllowUriTemplateToRemoveParameter() { var template = new UriTemplate("http://example.org/foo{?bar,baz}"); template.SetParameter("bar", "yo"); template.SetParameter("baz", "yuck"); template.ClearParameter("bar"); var uriString = template.Resolve(); Assert.Equal("http://example.org/foo?baz=yuck", uriString); } [Fact] public void ShouldAllowUriTemplateWithQueryParamsWithOneValue() { var template = new UriTemplate("http://example.org/foo{?bar,baz}"); template.SetParameter("baz", "yo"); var uriString = template.Resolve(); Assert.Equal("http://example.org/foo?baz=yo", uriString); } [Fact] public void LabelExpansionWithDotPrefixAndEmptyKeys() { var template = new UriTemplate("X{.empty_keys}"); template.SetParameter("empty_keys", new Dictionary()); var uriString = template.Resolve(); Assert.Equal("X", uriString); } [Fact] public void QueryParametersFromDictionary() { var template = new UriTemplate("http://example.org/customers{?query*}"); template.SetParameter("query", new Dictionary() { {"active","true"}, {"Country","Brazil"} }); var uriString = template.Resolve(); Assert.Equal("http://example.org/customers?active=true&Country=Brazil", uriString); } [Fact] public void ShouldAllowListAndSingleValueInQueryParam() { var template = new UriTemplate("http://example.org{/id*}{?fields,token}"); template.SetParameter("id", new List() { "person", "albums" }); template.SetParameter("fields", new List() { "id", "name", "picture" }); template.SetParameter("token", "12345"); var uriString = template.Resolve(); Assert.Equal("http://example.org/person/albums?fields=id,name,picture&token=12345", uriString); } [Fact] public void ShouldHandleUriEncoding() { var template = new UriTemplate("http://example.org/sparql{?query}"); template.SetParameter("query", "PREFIX dc: SELECT ?book ?who WHERE { ?book dc:creator ?who }"); var uriString = template.Resolve(); Assert.Equal("http://example.org/sparql?query=PREFIX%20dc%3A%20%3Chttp%3A%2F%2Fpurl.org%2Fdc%2Felements%2F1.1%2F%3E%20SELECT%20%3Fbook%20%3Fwho%20WHERE%20%7B%20%3Fbook%20dc%3Acreator%20%3Fwho%20%7D", uriString); } [Fact] public void ShouldHandleEncodingAParametersThatIsAUriWithAUriAsAParameter() { var template = new UriTemplate("http://example.org/go{?uri}"); template.SetParameter("uri", "http://example.org/?uri=http%3A%2F%2Fexample.org%2F"); var uriString = template.Resolve(); Assert.Equal("http://example.org/go?uri=http%3A%2F%2Fexample.org%2F%3Furi%3Dhttp%253A%252F%252Fexample.org%252F", uriString); } [Fact] public void ShouldThrowWhenExpressionIsNotClosed() { var result = string.Empty; try { var template = new UriTemplate("http://example.org/foo/{bar/baz/"); var uriString = template.Resolve(); } catch (ArgumentException ex) { result = ex.Message; } Assert.Equal("Malformed template, missing } : http://example.org/foo/{bar/baz/", result); } [Fact] public void ShouldThrowWhenTemplateExpressionIsEmpty() { var result = string.Empty; try { var template = new UriTemplate("http://example.org/foo/{}/baz/"); var uriString = template.Resolve(); } catch (ArgumentException ex) { result = ex.Message; } Assert.Equal("Malformed template : http://example.org/foo/{}/baz/", result); } [Fact] public void Query_param_with_exploded_array() { UriTemplate template = new UriTemplate("/foo/{foo}/baz{?haz*}"); template.SetParameter("foo", "1234"); template.SetParameter("haz", new string[] { "foo", "bar" }); string uri = template.Resolve(); Assert.Equal("/foo/1234/baz?haz=foo&haz=bar", uri); } [Fact] public void Query_param_with_list_array() { UriTemplate template = new UriTemplate("/foo/{foo}/baz{?haz}"); template.SetParameter("foo", "1234"); template.SetParameter("haz", new string[] { "foo", "bar" }); string uri = template.Resolve(); Assert.Equal("/foo/1234/baz?haz=foo,bar", uri); } [Fact] public void Query_param_with_empty_array() { UriTemplate template = new UriTemplate("/foo/{foo}/baz{?haz*}"); template.SetParameter("foo", "1234"); template.SetParameter("haz", new string[] { }); string uri = template.Resolve(); Assert.Equal("/foo/1234/baz", uri); } [Fact] public void ResolveOptionalAndRequiredQueryParameters() { UriTemplate template = new UriTemplate("https://api.github.com/search/code?q={query}{&page,per_page,sort,order}"); template.SetParameter("query", "1234"); template.SetParameter("per_page", "19"); var result = template.Resolve(); Assert.Equal("https://api.github.com/search/code?q=1234&per_page=19", result); } [Fact] public void ReservedCharacterExpansion() { UriTemplate template = new UriTemplate("https://foo.com/{?format}"); template.SetParameter("format", "application/vnd.foo+xml"); var result = template.Resolve(); Assert.Equal("https://foo.com/?format=application%2Fvnd.foo%2Bxml", result); } [Fact(Skip = "Unit tests should not require internet access!!")] public void PreserveReservedCharacterExpansion() { UriTemplate template = new UriTemplate("https://foo.com/?format={+format}"); template.SetParameter("format", "application/vnd.foo+xml"); var result = template.Resolve(); Assert.Equal("https://foo.com/?format=application/vnd.foo+xml", result); } [Fact] public void ShouldSupportUnicodeCharacters() { UriTemplate template = new UriTemplate("/lookup{?Stra%C3%9Fe}"); template.SetParameter("Stra%C3%9Fe", "Grüner Weg"); var result = template.Resolve(); Assert.Equal("/lookup?Stra%C3%9Fe=Gr%C3%BCner%20Weg", result); } // "assoc_special_chars" : { "šö䟜ñꀣ¥‡ÑÒÓÔÕ" : "ÖרÙÚàáâãäåæçÿ" } [Fact] public void ShouldSupportUnicodeCharacters2() { UriTemplate template = new UriTemplate("{?assoc_special_chars*}"); var dict = new Dictionary { {"šö䟜ñꀣ¥‡ÑÒÓÔÕ", "ÖרÙÚàáâãäåæçÿ"} }; template.SetParameter("assoc_special_chars", dict); var result = template.Resolve(); Assert.Equal("?%C5%A1%C3%B6%C3%A4%C5%B8%C5%93%C3%B1%C3%AA%E2%82%AC%C2%A3%C2%A5%E2%80%A1%C3%91%C3%92%C3%93%C3%94%C3%95=%C3%96%C3%97%C3%98%C3%99%C3%9A%C3%A0%C3%A1%C3%A2%C3%A3%C3%A4%C3%A5%C3%A6%C3%A7%C3%BF", result); } [Fact] public void Remove_a_query_parameters2() { var target = new Uri("http://example.org/customer?format=xml&id=23"); var template = target.MakeTemplate(); template.ClearParameter("format"); Assert.Equal("http://example.org/customer?id=23", template.Resolve()); } [Fact] public void EncodingTest1() { var url = new UriTemplate("/1/search/auto/{folder}{?query}") .AddParameter("folder", "My Documents") .AddParameter("query", "draft 2013") .Resolve(); Assert.Equal("/1/search/auto/My%20Documents?query=draft%202013", url); } [Fact] public void EncodingTest2() { // Parameter values get encoded but hyphen doesn't need to be encoded because it // is an "unreserved" character according to RFC 3986 var url = new UriTemplate("{/greeting}") .AddParameter("greeting", "hello-world") .Resolve(); Assert.Equal("/hello-world", url); // A slash does need to be encoded var url2 = new UriTemplate("{/greeting}") .AddParameter("greeting", "hello/world") .Resolve(); Assert.Equal("/hello%2Fworld", url2); // If you truly want to make multiple path segments then do this var url3 = new UriTemplate("{/greeting*}") .AddParameter("greeting", new List { "hello", "world" }) .Resolve(); Assert.Equal("/hello/world", url3); } // /docs/salary.csv?columns=1,2 // /docs/salary.csv?column=1&column=2 // /emails?from[name]=Don&from[date]=1998-03-24&to[name]=Norm // : /log?a=b&c=4 [Fact] public void EncodingTest3() { // There are different ways that lists can be included in query params // Just as a comma delimited list var url = new UriTemplate("/docs/salary.csv{?columns}") .AddParameter("columns", new List { 1, 2 }) .Resolve(); Assert.Equal("/docs/salary.csv?columns=1,2", url); // or as a multiple parameter instances var url2 = new UriTemplate("/docs/salary.csv{?columns*}") .AddParameter("columns", new List { 1, 2 }) .Resolve(); Assert.Equal("/docs/salary.csv?columns=1&columns=2", url2); } [Fact] public void EncodingTest4() { var url = new UriTemplate("/emails{?params*}") .AddParameter("params", new Dictionary { {"from[name]","Don"}, {"from[date]","1998-03-24"}, {"to[name]","Norm"} }) .Resolve(); Assert.Equal("/emails?from[name]=Don&from[date]=1998-03-24&to[name]=Norm", url); } [Fact] public void EncodingTest5() { ///log?a=b&c=4 var url = new UriTemplate("/log?a={a}&c={c}") .AddParameter("a", "b") .AddParameter("c", "4") .Resolve(); Assert.Equal("/log?a=b&c=4", url); } [Fact] public void InvalidSpace() { Exception ex = null; try { var url = new UriTemplate("/feeds/events{? fromId").Resolve(); } catch (Exception e) { ex = e; } Assert.NotNull(ex); } } } ================================================ FILE: src/UriTemplates/OperatorInfo.cs ================================================ namespace Tavis.UriTemplates { public class OperatorInfo { public bool Default { get; set; } public string First { get; set; } public char Separator { get; set; } public bool Named { get; set; } public string IfEmpty { get; set; } public bool AllowReserved { get; set; } } } ================================================ FILE: src/UriTemplates/QueryStringParameterOrder.cs ================================================ namespace Tavis.UriTemplates { public enum QueryStringParameterOrder { Strict, Any } } ================================================ FILE: src/UriTemplates/Result.cs ================================================ using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; namespace Tavis.UriTemplates { public class Result { public bool ErrorDetected { get; set; } public List ParameterNames { get; set; } private const string _UriReservedSymbols = ":/?#[]@!$&'()*+,;="; private const string _UriUnreservedSymbols = "-._~"; private StringBuilder _Result = new StringBuilder(); public Result() { ParameterNames = new List(); } public StringBuilder Append(char value) { return _Result.Append(value); } public StringBuilder Append(string value) { return _Result.Append(value); } public override string ToString() { return _Result.ToString(); } public void AppendName(string variable, OperatorInfo op, bool valueIsEmpty) { _Result.Append(variable); if (valueIsEmpty) { _Result.Append(op.IfEmpty); } else { _Result.Append("="); } } public void AppendList(OperatorInfo op, bool explode, string variable, IList list) { foreach (object item in list) { if (op.Named && explode) { _Result.Append(variable); _Result.Append("="); } AppendValue(item.ToString(), 0, op.AllowReserved); _Result.Append(explode ? op.Separator : ','); } if (list.Count > 0) { _Result.Remove(_Result.Length - 1, 1); } } public void AppendDictionary(OperatorInfo op, bool explode, IDictionary dictionary) { foreach (string key in dictionary.Keys) { _Result.Append(Encode(key, op.AllowReserved)); if (explode) _Result.Append('='); else _Result.Append(','); AppendValue(dictionary[key], 0, op.AllowReserved); if (explode) { _Result.Append(op.Separator); } else { _Result.Append(','); } } if (dictionary.Count() > 0) { _Result.Remove(_Result.Length - 1, 1); } } public void AppendValue(string value, int prefixLength, bool allowReserved) { if (prefixLength != 0) { if (prefixLength < value.Length) { value = value.Substring(0, prefixLength); } } _Result.Append(Encode(value, allowReserved)); } private static string Encode(string p, bool allowReserved) { var result = new StringBuilder(); foreach (char c in p) { if ((c >= 'A' && c <= 'z') //Alpha || (c >= '0' && c <= '9') // Digit || _UriUnreservedSymbols.IndexOf(c) != -1 // Unreserved symbols - These should never be percent encoded || (allowReserved && _UriReservedSymbols.IndexOf(c) != -1)) // Reserved symbols - should be included if requested (+) { result.Append(c); } else { var bytes = Encoding.UTF8.GetBytes(new[] { c }); foreach (var abyte in bytes) { result.Append(HexEscape(abyte)); } } } return result.ToString(); } public static string HexEscape(byte i) { var esc = new char[3]; esc[0] = '%'; esc[1] = HexDigits[((i & 240) >> 4)]; esc[2] = HexDigits[(i & 15)]; return new string(esc); } public static string HexEscape(char c) { var esc = new char[3]; esc[0] = '%'; esc[1] = HexDigits[(((int)c & 240) >> 4)]; esc[2] = HexDigits[((int)c & 15)]; return new string(esc); } private static readonly char[] HexDigits = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; } } ================================================ FILE: src/UriTemplates/UriTemplate.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; namespace Tavis.UriTemplates { using System; using System.Collections; using System.Collections.Generic; #if TYPE_CONVERTER using System.ComponentModel; #endif using System.Linq; using System.Text; #if TYPE_CONVERTER [TypeConverter(typeof(UriTemplateConverter))] #endif public class UriTemplate { private static Dictionary _Operators = new Dictionary() { {'\0', new OperatorInfo {Default = true, First = "", Separator = ',', Named = false, IfEmpty = "",AllowReserved = false}}, {'+', new OperatorInfo {Default = false, First = "", Separator = ',', Named = false, IfEmpty = "",AllowReserved = true}}, {'.', new OperatorInfo {Default = false, First = ".", Separator = '.', Named = false, IfEmpty = "",AllowReserved = false}}, {'/', new OperatorInfo {Default = false, First = "/", Separator = '/', Named = false, IfEmpty = "",AllowReserved = false}}, {';', new OperatorInfo {Default = false, First = ";", Separator = ';', Named = true, IfEmpty = "",AllowReserved = false}}, {'?', new OperatorInfo {Default = false, First = "?", Separator = '&', Named = true, IfEmpty = "=",AllowReserved = false}}, {'&', new OperatorInfo {Default = false, First = "&", Separator = '&', Named = true, IfEmpty = "=",AllowReserved = false}}, {'#', new OperatorInfo {Default = false, First = "#", Separator = ',', Named = false, IfEmpty = "",AllowReserved = true}} }; private readonly string _template; private readonly Dictionary _Parameters; private enum States { CopyingLiterals, ParsingExpression } private readonly bool _resolvePartially; public UriTemplate(string template, bool resolvePartially = false, bool caseInsensitiveParameterNames = false) { _resolvePartially = resolvePartially; _template = template; _Parameters = caseInsensitiveParameterNames ? new Dictionary(StringComparer.OrdinalIgnoreCase) : new Dictionary(); } public override string ToString() { return _template; } public void SetParameter(string name, object value) { _Parameters[name] = value; } public void ClearParameter(string name) { _Parameters.Remove(name); } public void SetParameter(string name, string value) { _Parameters[name] = value; } public void SetParameter(string name, IEnumerable value) { _Parameters[name] = value; } public void SetParameter(string name, IDictionary value) { _Parameters[name] = value; } public IEnumerable GetParameterNames() { Result result = ResolveResult(); return result.ParameterNames; } public string Resolve() { var result = ResolveResult(); return result.ToString(); } private Result ResolveResult() { var currentState = States.CopyingLiterals; var result = new Result(); StringBuilder currentExpression = null; foreach (var character in _template.ToCharArray()) { switch (currentState) { case States.CopyingLiterals: if (character == '{') { currentState = States.ParsingExpression; currentExpression = new StringBuilder(); } else if (character == '}') { throw new ArgumentException("Malformed template, unexpected } : " + result.ToString()); } else { result.Append(character); } break; case States.ParsingExpression: if (character == '}') { ProcessExpression(currentExpression, result); currentState = States.CopyingLiterals; } else { currentExpression.Append(character); } break; } } if (currentState == States.ParsingExpression) { result.Append("{"); result.Append(currentExpression.ToString()); throw new ArgumentException("Malformed template, missing } : " + result.ToString()); } if (result.ErrorDetected) { throw new ArgumentException("Malformed template : " + result.ToString()); } return result; } private void ProcessExpression(StringBuilder currentExpression, Result result) { if (currentExpression.Length == 0) { result.ErrorDetected = true; result.Append("{}"); return; } OperatorInfo op = GetOperator(currentExpression[0]); var firstChar = op.Default ? 0 : 1; bool multivariableExpression = false; var varSpec = new VarSpec(op); for (int i = firstChar; i < currentExpression.Length; i++) { char currentChar = currentExpression[i]; switch (currentChar) { case '*': varSpec.Explode = true; break; case ':': // Parse Prefix Modifier var prefixText = new StringBuilder(); currentChar = currentExpression[++i]; while (currentChar >= '0' && currentChar <= '9' && i < currentExpression.Length) { prefixText.Append(currentChar); i++; if (i < currentExpression.Length) currentChar = currentExpression[i]; } varSpec.PrefixLength = int.Parse(prefixText.ToString()); i--; break; case ',': multivariableExpression = true; var success = ProcessVariable(varSpec, result, multivariableExpression); bool isFirst = varSpec.First; // Reset for new variable varSpec = new VarSpec(op); if (success || !isFirst || _resolvePartially) varSpec.First = false; if (!success && _resolvePartially) { result.Append(","); } break; default: if (IsVarNameChar(currentChar)) { varSpec.VarName.Append(currentChar); } else { result.ErrorDetected = true; } break; } } ProcessVariable(varSpec, result, multivariableExpression); if (multivariableExpression && _resolvePartially) result.Append("}"); } private bool ProcessVariable(VarSpec varSpec, Result result, bool multiVariableExpression = false) { var varname = varSpec.VarName.ToString(); result.ParameterNames.Add(varname); if (!_Parameters.ContainsKey(varname) || _Parameters[varname] == null || (_Parameters[varname] is IList && ((IList)_Parameters[varname]).Count == 0) || (_Parameters[varname] is IDictionary && ((IDictionary)_Parameters[varname]).Count == 0)) { if (_resolvePartially == true) { if (multiVariableExpression) { if (varSpec.First) { result.Append("{"); } result.Append(varSpec.ToString()); } else { result.Append("{"); result.Append(varSpec.ToString()); result.Append("}"); } return false; } return false; } if (varSpec.First) { result.Append(varSpec.OperatorInfo.First); } else { result.Append(varSpec.OperatorInfo.Separator); } object value = _Parameters[varname]; // Handle Strings if (value is string) { var stringValue = (string)value; if (varSpec.OperatorInfo.Named) { result.AppendName(varname, varSpec.OperatorInfo, string.IsNullOrEmpty(stringValue)); } result.AppendValue(stringValue, varSpec.PrefixLength, varSpec.OperatorInfo.AllowReserved); } else { // Handle Lists var list = value as IList; if (list == null && value is IEnumerable) { list = ((IEnumerable)value).ToList(); }; if (list != null) { if (varSpec.OperatorInfo.Named && !varSpec.Explode) // exploding will prefix with list name { result.AppendName(varname, varSpec.OperatorInfo, list.Count == 0); } result.AppendList(varSpec.OperatorInfo, varSpec.Explode, varname, list); } else { // Handle associative arrays var dictionary = value as IDictionary; if (dictionary != null) { if (varSpec.OperatorInfo.Named && !varSpec.Explode) // exploding will prefix with list name { result.AppendName(varname, varSpec.OperatorInfo, dictionary.Count() == 0); } result.AppendDictionary(varSpec.OperatorInfo, varSpec.Explode, dictionary); } else { // If above all fails, convert the object to string using the default object.ToString() implementation var stringValue = value.ToString(); if (varSpec.OperatorInfo.Named) { result.AppendName(varname, varSpec.OperatorInfo, string.IsNullOrEmpty(stringValue)); } result.AppendValue(stringValue, varSpec.PrefixLength, varSpec.OperatorInfo.AllowReserved); } } } return true; } private static bool IsVarNameChar(char c) { return ((c >= 'A' && c <= 'z') //Alpha || (c >= '0' && c <= '9') // Digit || c == '_' || c == '%' || c == '.'); } private static OperatorInfo GetOperator(char operatorIndicator) { OperatorInfo op; switch (operatorIndicator) { case '+': case ';': case '/': case '#': case '&': case '?': case '.': op = _Operators[operatorIndicator]; break; default: op = _Operators['\0']; break; } return op; } private const string varname = "[a-zA-Z0-9_]*"; private const string op = "(?[+#./;?&]?)"; private const string var = "(?(?:(?" + varname + ")[*]?,?)*)"; private const string varspec = "(?{" + op + var + "})"; // (?{(?[+#./;?&]?)(?[a-zA-Z0-9_]*[*]?|(?:(?[a-zA-Z0-9_]*[*]?),?)*)}) private Regex _ParameterRegex = null; private Uri _ComponentBaseUri = new Uri("https://localhost.com", UriKind.Absolute); public IDictionary GetParameters(Uri uri, QueryStringParameterOrder order = QueryStringParameterOrder.Strict) { switch (order) { case QueryStringParameterOrder.Strict: { if (_ParameterRegex == null) { var matchingRegex = CreateMatchingRegex(_template); lock (this) { _ParameterRegex = new Regex(matchingRegex); } } var match = _ParameterRegex.Match(uri.OriginalString); var parameters = new Dictionary(); for (int x = 1; x < match.Groups.Count; x++) { if (match.Groups[x].Success) { var paramName = _ParameterRegex.GroupNameFromNumber(x); if (!string.IsNullOrEmpty(paramName)) { parameters.Add(paramName, Uri.UnescapeDataString(match.Groups[x].Value)); } } } return match.Success ? parameters : null; } case QueryStringParameterOrder.Any: { if (!uri.IsAbsoluteUri) uri = new Uri(_ComponentBaseUri, uri); var uriString = uri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path | UriComponents.Fragment, UriFormat.UriEscaped); var uriWithoutQuery = new Uri(uriString, UriKind.Absolute); var pathParameters = GetParameters(uriWithoutQuery) ?? new Dictionary(_Parameters.Comparer); Result result = ResolveResult(); var parameterNames = result.ParameterNames; foreach (var parameter in uri.GetQueryStringParameters()) { if (!parameterNames.Contains(parameter.Key)) continue; pathParameters.Add(parameter.Key, parameter.Value); } return pathParameters.Count == 0 ? null : pathParameters; } default: throw new ArgumentOutOfRangeException(nameof(order), order, null); } } public static string CreateMatchingRegex(string uriTemplate) { var findParam = new Regex(varspec); var template = new Regex(@"([^{]|^)\?").Replace(uriTemplate, @"$+\?"); ;//.Replace("?",@"\?"); var regex = findParam.Replace(template, delegate (Match m) { var paramNames = m.Groups["lvar"].Captures.Cast().Where(c => !string.IsNullOrEmpty(c.Value)).Select(c => c.Value).ToList(); var op = m.Groups["op"].Value; switch (op) { case "?": return GetQueryExpression(paramNames, prefix: "?"); case "&": return GetQueryExpression(paramNames, prefix: "&"); case "#": return GetExpression(paramNames, prefix: "#"); case "/": return GetExpression(paramNames, prefix: "/"); case "+": return GetExpression(paramNames); default: return GetExpression(paramNames); } }); return regex + "$"; } public static string CreateMatchingRegex2(string uriTemplate) { var findParam = new Regex(varspec); //split by host/path/query/fragment var template = new Regex(@"([^{]|^)\?").Replace(uriTemplate, @"$+\?"); ;//.Replace("?",@"\?"); var regex = findParam.Replace(template, delegate (Match m) { var paramNames = m.Groups["lvar"].Captures.Cast().Where(c => !string.IsNullOrEmpty(c.Value)).Select(c => c.Value).ToList(); var op = m.Groups["op"].Value; switch (op) { case "?": return GetQueryExpression(paramNames, prefix: "?"); case "&": return GetQueryExpression(paramNames, prefix: "&"); case "#": return GetExpression(paramNames, prefix: "#"); case "/": return GetExpression(paramNames, prefix: "/"); case "+": return GetExpression(paramNames); default: return GetExpression(paramNames); } }); return regex + "$"; } private static string GetQueryExpression(List paramNames, string prefix) { StringBuilder sb = new StringBuilder(); foreach (var paramname in paramNames) { sb.Append(@"\" + prefix + "?"); if (prefix == "?") prefix = "&"; sb.Append("(?:"); sb.Append(paramname); sb.Append("="); sb.Append("(?<"); sb.Append(paramname); sb.Append(">"); sb.Append("[^/?&]+"); sb.Append(")"); sb.Append(")?"); } return sb.ToString(); } private static string GetExpression(List paramNames, string prefix = null) { StringBuilder sb = new StringBuilder(); string paramDelim; switch (prefix) { case "#": paramDelim = "[^,]+"; break; case "/": paramDelim = "[^/?]+"; break; case "?": case "&": paramDelim = "[^&#]+"; break; case ";": paramDelim = "[^;/?#]+"; break; case ".": paramDelim = "[^./?#]+"; break; default: paramDelim = "[^/?&]+"; break; } foreach (var paramname in paramNames) { if (string.IsNullOrEmpty(paramname)) continue; if (prefix != null) { sb.Append(@"\" + prefix + "?"); if (prefix == "#") { prefix = ","; } } sb.Append("(?<"); sb.Append(paramname); sb.Append(">"); sb.Append(paramDelim); // Param Value sb.Append(")?"); } return sb.ToString(); } } } ================================================ FILE: src/UriTemplates/UriTemplateConverter.cs ================================================ using System; using System.ComponentModel; using System.Globalization; namespace Tavis.UriTemplates { #if TYPE_CONVERTER /// /// Converts to instances from other representations. /// public sealed class UriTemplateConverter : TypeConverter { /// public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { return sourceType == typeof(string); } /// public override object ConvertFrom( ITypeDescriptorContext context, CultureInfo culture, object value) { if (value == null) { return null; } var template = value as string; if (template != null) { if (template.Length == 0) { // For TypeConverter purposes, an empty string is "no value." return null; } return new UriTemplate(template); } throw (NotSupportedException)GetConvertFromException(value); } } #endif } ================================================ FILE: src/UriTemplates/UriTemplateExtensions.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; namespace Tavis.UriTemplates { public static class UriTemplateExtensions { public static UriTemplate AddParameter(this UriTemplate template, string name, object value) { template.SetParameter(name, value); return template; } public static UriTemplate AddParameters(this UriTemplate template, object parametersObject) { if (parametersObject != null) { IEnumerable properties; #if NETSTANDARD1_0 var type = parametersObject.GetType().GetTypeInfo(); properties = type.DeclaredProperties.Where(p=> p.CanRead); #else properties = parametersObject.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); #endif foreach (var propinfo in properties) { template.SetParameter(propinfo.Name, propinfo.GetValue(parametersObject, null)); } } return template; } public static UriTemplate AddParameters(this UriTemplate uriTemplate, IDictionary linkParameters) { if (linkParameters != null) { foreach (var parameter in linkParameters) { uriTemplate.SetParameter(parameter.Key, parameter.Value); } } return uriTemplate; } } public static class UriExtensions { public static UriTemplate MakeTemplate(this Uri uri) { var parameters = uri.GetQueryStringParameters(); return MakeTemplate(uri, parameters); } public static UriTemplate MakeTemplate(this Uri uri, IDictionary parameters) { var target = uri.GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Query & ~UriComponents.Fragment, UriFormat.Unescaped); var template = new UriTemplate(target + "{?" + string.Join(",", parameters.Keys.ToArray()) + "}"); template.AddParameters(parameters); return template; } public static Dictionary GetQueryStringParameters(this Uri target) { Uri uri = target; var parameters = new Dictionary(); var reg = new Regex(@"([-A-Za-z0-9._~]*)=([^&]*)&?"); // Unreserved characters: http://tools.ietf.org/html/rfc3986#section-2.3 foreach (Match m in reg.Matches(uri.Query)) { string key = m.Groups[1].Value.ToLowerInvariant(); string value = m.Groups[2].Value; parameters.Add(key, value); } return parameters; } } } ================================================ FILE: src/UriTemplates/UriTemplateTable.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Tavis.UriTemplates { public class UriTemplateTable { private Dictionary _Templates = new Dictionary(); public void Add(string key, UriTemplate template) { _Templates.Add(key, template); } public TemplateMatch Match(Uri url, QueryStringParameterOrder order = QueryStringParameterOrder.Strict) { foreach (var template in _Templates) { var parameters = template.Value.GetParameters(url, order); if (parameters != null) { return new TemplateMatch() { Key = template.Key, Parameters = parameters, Template = template.Value }; } } return null; } public UriTemplate this[string key] { get { UriTemplate value; if (_Templates.TryGetValue(key, out value)) { return value; } else { return null; } } } } public class TemplateMatch { public string Key { get; set; } public UriTemplate Template { get; set; } public IDictionary Parameters { get; set; } } } ================================================ FILE: src/UriTemplates/UriTemplates.csproj ================================================ netstandard2.0;netstandard2.1; true Tavis Tavis.UriTemplates URI Template resolution library - Implementation of RFC 6570 Darrel Miller 2.0.0 https://github.com/tavis-software/Tavis.UriTemplates true LICENSE README.md git true https://github.com/tavis-software/Tavis.UriTemplates true snupkg true true ../../UriTemplateKey.snk runtime; build; native; contentfiles; analyzers; buildtransitive all DEBUG;TRACE;TYPE_CONVERTER true ================================================ FILE: src/UriTemplates/VarSpec.cs ================================================ using System.Text; namespace Tavis.UriTemplates { public class VarSpec { private readonly OperatorInfo _operatorInfo; public StringBuilder VarName = new StringBuilder(); public bool Explode = false; public int PrefixLength = 0; public bool First = true; public VarSpec(OperatorInfo operatorInfo) { _operatorInfo = operatorInfo; } public OperatorInfo OperatorInfo { get { return _operatorInfo; } } public override string ToString() { return (First ? _operatorInfo.First : "") + VarName.ToString() + (Explode ? "*" : "") + (PrefixLength > 0 ? ":" + PrefixLength : ""); } } }