diff --git a/.gitattributes b/.gitattributes old mode 100644 new mode 100755 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..7b66a62f --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +buy_me_a_coffee: vXCNnz9 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..87bc8a46 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,39 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: 'type: bug' +assignees: zapadi + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Call the Redmine API endpoint '...' +2. Call the client method: `...` +3. Inspect the returned response/data +4. Notice the incorrect behavior or error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Sample Code / Request** +If applicable, add a minimal code snippet or HTTP request that reproduces the issue. + +**API Response / Error** +If applicable, paste the response body, error message, or stack trace. + +**Environment (please complete the following information):** +- Library version: [e.g. 4.50.0] +- .NET runtime: [e.g. .NET 8.0, .NET Framework 4.8] +- Redmine version: [e.g. 5.1.2] +- OS: [e.g. Windows 11, Ubuntu 22.04] + +**Screenshots** +If applicable, add screenshots to help explain the problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..87bd1b13 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,28 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[Feature]" +labels: 'type: enhancement, type: feature' +assignees: zapadi + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. +Ex: "Add support for calling the `...` endpoint so that I can [...]" + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. +Ex: "Right now I make a raw HTTP request using `HttpClient`, but it would be better if the client library exposed [...]" + +**Proposed API / Usage Example (if applicable)** +If relevant, provide a code snippet, method signature, or example API request/response. +```csharp +var issue = await client.Issues.GetByIdAsync(123, includeChildren: true); +``` + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml new file mode 100644 index 00000000..bdc96713 --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,123 @@ +name: 'Build and Test' + +on: + workflow_call: + workflow_dispatch: + inputs: + reason: + description: 'The reason for running the workflow' + required: false + default: 'Manual build and run tests' + push: + tags-ignore: + - '[0-9]+.[0-9]+.[0-9]+*' + paths: + - '**.cs' + - '**.csproj' + - '**.sln' + pull_request: + branches: [ master ] + paths: + - '**.cs' + - '**.csproj' + - '**.sln' + +# concurrency: +# group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} +# cancel-in-progress: true + +env: + # Disable the .NET logo in the console output. + DOTNET_NOLOGO: true + + # Stop wasting time caching packages + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + + # Disable sending usage data to Microsoft + DOTNET_CLI_TELEMETRY_OPTOUT: true + + DOTNET_ADD_GLOBAL_TOOLS_TO_PATH: false + + DOTNET_MULTILEVEL_LOOKUP: 0 + + PROJECT_PATH: . + + CONFIGURATION: Release + +jobs: + before: + name: Before + runs-on: ubuntu-latest + steps: + - name: Info Before + run: | + echo "[${{ github.event_name }}] event automatically triggered this job." + echo "branch name is ${{ github.ref }}" + echo "This job has a '${{ job.status }}' status." + - name: Run a one-line script + run: | + echo "Is true: $( [ \"$EVENT_NAME\" = 'push' ] && [ \"$GITHUB_REF\" != 'refs/tags/' ] ) || [ \"$EVENT_NAME\" = 'workflow_dispatch' ]" + env: + EVENT_NAME: ${{ github.event_name }} + GITHUB_REF: ${{ github.ref }} + + build: + needs: before + name: Build ${{ matrix.os }} - dotnet ${{ matrix.dotnet }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest, windows-latest, macos-latest ] + + steps: + - name: Print manual run reason + if: ${{ github.event_name == 'workflow_dispatch' }} + run: | + echo 'Reason: ${{ github.event.inputs.reason }}' + + - name: Checkout + uses: actions/checkout@v4 + with: + lfs: true + fetch-depth: 0 + + - name: Setup .NET (global.json) + uses: actions/setup-dotnet@v4 + + - name: Display dotnet version + run: dotnet --version + + - uses: actions/cache@v4 + with: + path: ~/.nuget/packages + # Look to see if there is a cache hit for the corresponding requirements file + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + restore-keys: | + ${{ runner.os }}-nuget + + - name: Restore + run: dotnet restore "${{ env.PROJECT_PATH }}" + + - name: πŸ”¨ Build + run: >- + dotnet build "${{ env.PROJECT_PATH }}" + --configuration "${{ env.CONFIGURATION }}" + --no-restore + + - name: Test + timeout-minutes: 60 + run: >- + dotnet test "${{ env.PROJECT_PATH }}" + --no-restore + --no-build + --verbosity normal + --logger trx + --results-directory "TestResults-${{ matrix.os }}" || true + + - name: Upload test results + if: ${{ always() }} + uses: actions/upload-artifact@v4 + with: + name: test-results-${{ matrix.os }} + path: TestResults-${{ matrix.os }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..4c4bd893 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,70 @@ +# 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: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '34 7 * * 6' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'csharp' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # 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@v3 + + # ℹ️ 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 + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..a20b2882 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,178 @@ +name: 'Publish to NuGet' + +on: + workflow_dispatch: + inputs: + reason: + description: 'The reason for running the workflow' + required: false + default: 'Manual publish' + version: + description: 'Version' + required: true + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+*' + +env: + # Disable the .NET logo in the console output. + DOTNET_NOLOGO: true + + # Disable sending usage data to Microsoft + DOTNET_CLI_TELEMETRY_OPTOUT: true + + # Set working directory + PROJECT_PATH: ./src/redmine-net-api/redmine-net-api.csproj + + # Configuration + CONFIGURATION: Release + +jobs: + check-tag-branch: + name: Check Tag and Master Branch hashes + # This job is based on replies in https://github.community/t/how-to-create-filter-on-both-tag-and-branch/16936/6 + runs-on: ubuntu-latest + outputs: + ver: ${{ steps.set-version.outputs.VERSION }} + steps: + - name: Get tag commit hash + id: tag-commit-hash + run: | + hash=${{ github.sha }} + echo "{name}=tag-hash::${hash}" >> $GITHUB_OUTPUT + echo $hash + + - name: Checkout master + uses: actions/checkout@v4 + with: + ref: master + + - name: Get latest master commit hash + id: master-commit-hash + run: | + hash=$(git log -n1 --format=format:"%H") + echo "{name}=master-hash::${hash}" >> $GITHUB_OUTPUT + echo $hash + + - name: Verify tag commit matches master commit - exit if they don't match + if: steps.tag-commit-hash.outputs.tag-hash != steps.master-commit-hash.outputs.master-hash + run: | + echo "Tag was not on the master branch. Exiting." + exit 1 + + - name: Get Dispatched Version + if: github.event_name == 'workflow_dispatch' + run: | + echo "VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV + + - name: Get Tag Version + if: github.event_name == 'push' + run: | + echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + + - name: Set Version + id: set-version + run: | + echo "VERSION=${{ env.VERSION }}" >> "$GITHUB_OUTPUT" + + validate-version: + name: Validate Version + needs: check-tag-branch + runs-on: ubuntu-latest + steps: + - name: Get Version + run: echo "VERSION=${{ needs.check-tag-branch.outputs.ver }}" >> $GITHUB_ENV + + - name: Display Version + run: echo "$VERSION" + + - name: Check Version Is Declared + run: | + if [[ -z "$VERSION" ]]; then + echo "Version is not declared." + exit 1 + fi + + - name: Validate Version matches SemVer format + run: | + if [[ ! "$VERSION" =~ ^([0-9]+\.){2,3}[0-9]+(-[a-zA-Z0-9.-]+)*$ ]]; then + echo "The version does not match the SemVer format (X.Y.Z). Please provide a valid version." + exit 1 + fi + + call-build-and-test: + name: Call Build and Test + needs: validate-version + uses: ./.github/workflows/build-and-test.yml + + pack: + name: Pack + needs: [check-tag-branch, validate-version, call-build-and-test] + runs-on: ubuntu-latest + steps: + - name: Get Version + run: echo "VERSION=${{ needs.check-tag-branch.outputs.ver }}" >> $GITHUB_ENV + + - name: Checkout + uses: actions/checkout@v4 + with: + lfs: true + fetch-depth: 0 + + - name: Setup .NET Core (global.json) + uses: actions/setup-dotnet@v4 + + - name: Display dotnet version + run: dotnet --version + + - name: Install dependencies + run: dotnet restore "${{ env.PROJECT_PATH }}" + + - name: πŸ“¦ Create the package + run: >- + dotnet pack "${{ env.PROJECT_PATH }}" + --output ./artifacts + --configuration "${{ env.CONFIGURATION }}" + --include-symbols + --include-source + -p:Version=$VERSION + -p:PackageVersion=$VERSION + -p:IncludeSymbols=true + -p:SymbolPackageFormat=snupkg + + - name: πŸ“¦ Create the package - Signed + run: >- + dotnet pack "${{ env.PROJECT_PATH }}" + --output ./artifacts + --configuration "${{ env.CONFIGURATION }}" + --include-symbols + --include-source + -p:Version=$VERSION + -p:PackageVersion=$VERSION + -p:IncludeSymbols=true + -p:SymbolPackageFormat=snupkg + -p:Sign=true + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: artifacts + path: ./artifacts + + publish: + name: Publish to Nuget + needs: pack + runs-on: ubuntu-latest + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts + path: ./artifacts + + - name: Publish packages + run: >- + dotnet nuget push ./artifacts/**.nupkg + --source 'https://api.nuget.org/v3/index.json' + --api-key ${{secrets.NUGET_TOKEN}} + --skip-duplicate diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index 27110cda..1008d646 --- a/.gitignore +++ b/.gitignore @@ -260,3 +260,4 @@ paket-files/ ### VisualStudioCode ### .vscode +.artifacts diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..a423cf3f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,91 @@ +# Changelog + +## [v4.4.0] + +Added: +* Added ParentTitle to wiki page + +Breaking Changes: + +* Changed ChangeSet revision type from int to string + +## [v4.3.0] + +Added: +* Added WikiPageTitle, EstimatedHours & SpentHours to version +* Added IsAdmin, TwoFactorAuthenticationScheme, PasswordChangedOn, UpdatedOn to user +* Added IsActive to time entry activity +* Added IssuesVisibility, TimeEntriesVisibility, UsersVisibility & IsAssignable to role +* Added DefaultAssignee & DefaultVersion to project +* Added MyAccount type +* Added attachments & comments to news +* Added AllowedStatuses to issue +* Added search type + +Fixes: +* Issue Relations Read Error for Copied Issues (Relation Type : copied_to) (#288) + + +## [v4.2.3] + +Fixes: +* The only milliseconds component is set to Timeout. (#284) + +## [v4.2.2] + +Fixes: + +* GetObjectsAsync raises ArgumentNullException when should return null (#280) + +## [v4.2.1] + +* Small fixes. + +## [v4.2.0] + +* Small refactoring + +## [v4.1.0] + +Fixes: + +* Assigning IssueCustomFields to a project should be supported (#277) +* How to add a custom field to a project (#276) +* Wrong encoding of special characters in URLs causes 404 (#274) + +## [v4.0.2] + +Fixes: Add #236 to current version. + +## [v4.0.1] + +Fixes: + +* JSON serialization exception for issues with uploads (missing WriteStart/EndObject calls) (#271) (thanks muffmolch) + +## [v4.0.0] + +Features: + +* Add support for .NET Standard 2.0 and 2.1 + +Fixes: + +* Trackers - Cannot retreive List of trackers: Malformed objects (#265) (thanks NecatiMeral) +* IssueRelation - `relation_type` cannot be parsed (#263) (thanks NecatiMeral) +* Issue with 'relates" relation (#262) (thanks NecatiMeral) +* RedmineManager.GetObjects<>(params string[]) only retrieves 25 objects (#260) +* Unexpected ArgumentNullException in RedmineManager.cs:581 (#259) +* Type Issue and its property ParentIssue have no matching base-type (#258) +* Cannot set the name of the project (#257) +* Cant create new issue (#256) +* Help me with create issues api redmine. Error is The property or indexer 'Identifiable.Id' cannot be used in this context because the set accessor is inaccessible (#254) +* Version 3.0.6.1 makes IssueCustomField.Info readonly breaking existing usage (#253) +* Empty response on CreateOrUpdateWikiPage (#245) +* Cannot set the status of a project (#255) +* Could not deserialize null!' When update WikiPage (#225) + +Breaking Changes: + +* Split CreateOrUpdateWikiPage into CreateWikiPage & UpdateWikiPage +* Add IdentifiableName.Create(id) in order to create identifiablename types with id. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md old mode 100644 new mode 100755 diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..f2b9a699 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,21 @@ + + + + 12 + strict + true + + + + true + true + true + true + + + + + + + + \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 00000000..d5faa965 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,29 @@ + + + |net20|net40|net45|net451|net452|net46| + |net20|net40|net45|net451|net452|net46|net461| + |net45|net451|net452|net46| + |net45|net451|net452|net46|net461| + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..9e322a34 --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,6 @@ +When creating a new issue, please make sure the following information is part of your issue description (if applicable). + +- Which Redmine server version are you using +- Which Redmine.Net.Api version are you using +- Which serialization type (xml or json) are you using +- A list of steps or a gist or a github repository which can be easily used to reproduce your case. \ No newline at end of file diff --git a/NuGet/NuGet.exe b/NuGet/NuGet.exe deleted file mode 100755 index 6bb79fe5..00000000 Binary files a/NuGet/NuGet.exe and /dev/null differ diff --git a/NuGet/redmine-api-signed.nupkg b/NuGet/redmine-api-signed.nupkg deleted file mode 100755 index 016db320..00000000 Binary files a/NuGet/redmine-api-signed.nupkg and /dev/null differ diff --git a/NuGet/redmine-api.0.0.0.0.nupkg b/NuGet/redmine-api.0.0.0.0.nupkg deleted file mode 100755 index 17ed4fcf..00000000 Binary files a/NuGet/redmine-api.0.0.0.0.nupkg and /dev/null differ diff --git a/NuGet/redmine-api.nupkg b/NuGet/redmine-api.nupkg deleted file mode 100755 index 6bef6045..00000000 Binary files a/NuGet/redmine-api.nupkg and /dev/null differ diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..a3f3ba2e --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,33 @@ +We welcome contributions! + +Here's how you can help: + +## Description + + +## Related Issues + +- Closes #ISSUE_ID +- Fixes #ISSUE_ID + +## Type of Change + +- [ ] πŸ› Bug fix (non-breaking change which fixes an issue) +- [ ] ✨ New feature (non-breaking change which adds functionality) +- [ ] πŸ’₯ Breaking change (fix or feature that would cause existing functionality to change) +- [ ] 🧹 Code cleanup / refactor +- [ ] πŸ“– Documentation update + +## Checklist + +- [ ] I have tested my changes locally against a Redmine instance. +- [ ] I have added/updated unit tests if applicable. +- [ ] I have updated documentation (README / XML comments / wiki). +- [ ] I followed the project’s coding style. +- [ ] New and existing tests pass with my changes. + +## API Changes (if applicable) + +```csharp +// Example of a new/updated method signature +Task GetByIdAsync(int id, bool includeChildren = false); diff --git a/README.md b/README.md index 499509b7..9a2add82 100755 --- a/README.md +++ b/README.md @@ -1,72 +1,119 @@ -![](https://github.com/zapadi/redmine-net-api/blob/master/logo.png) -# redmine-net-api - -redmine-net-api is a library for communicating with a Redmine project management application. - -* Uses [Redmine's REST API.](http://www.redmine.org/projects/redmine/wiki/Rest_api/) -* Supports both XML and **JSON(requires .NET Framework 3.5 or higher)** formats. -* Supports GZipped responses from servers. -* This API provides access and basic CRUD operations (create, read, update, delete) for the resources described below: - -Resource | Read | Create | Update | Delete ----------|------|--------|--------|------- - Attachments|x|x|-|- - Custom Fields|x|-|-|- - Enumerations |x|-|-|- - Groups|x|x|x|x - Issues |x|x|x|x - Issue Categories|x|x|x|x - Issue Relations|x|x|x|x - Issue Statuses|x|-|-|- - News|x|-|-|- - Projects|x|x|x|x - Project Memberships|x|x|x|x - Queries |x|-|-|- - Roles |x|-|-|- - Time Entries |x|x|x|x - Trackers |x|-|-|- - Users |x|x|x|x - Versions |x|x|x|x - Wiki Pages |x|x|x|x - Files |x|x|-|- - -## Packages and Status - -Package | Build status | Nuget --------- | ------------ | ------- -redmine-net20-api | ![alt text](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true) | [![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) -redmine-net40-api | ![alt text](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true) | [![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) -redmine-net40-api-signed | ![alt text](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true) | [![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) -redmine-net45-api | ![alt text](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true) | [![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) -redmine-net45-api-signed | ![alt text](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true) | [![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) -redmine-net451-api | ![alt text](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true) | [![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) -redmine-net451-api-signed | ![alt text](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true) | [![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) -redmine-net452-api | ![alt text](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true) | [![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) -redmine-net452-api-signed | ![alt text](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true) | [![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) - -## WIKI - -Please review the wiki pages on how to use **redmine-net-api**. - -## Contributing -Contributions are really appreciated! - -A good way to get started (flow): - -1. Fork the redmine-net-api repository. -2. Create a new branch in your current repos from the 'master' branch. -3. 'Check out' the code with *Git*, *GitHub Desktop* or *SourceTree*. -4. Push commits and create a Pull Request (PR) to redmine-net-api. - -## License -[![redmine-net-api](https://img.shields.io/hexpm/l/plug.svg)]() - -The API is released under Apache 2 open-source license. You can use it for both personal and commercial purposes, build upon it and modify it. - -## Thanks - -I would like to thank: - -* JetBrains for my Open Source ReSharper licence, - -* AppVeyor for allowing free build CI services for Open Source projects +# ![Redmine .NET API](https://raw.githubusercontent.com/zapadi/redmine-net-api/master/logo.png) redmine-net-api + +[![NuGet](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) +[![NuGet Downloads](https://img.shields.io/nuget/dt/redmine-api)](https://www.nuget.org/packages/redmine-api) +[![License](https://img.shields.io/github/license/zapadi/redmine-net-api)](LICENSE) +[![Contributors](https://img.shields.io/github/contributors/zapadi/redmine-net-api)](https://github.com/zapadi/redmine-net-api/graphs/contributors) + + +A modern and flexible .NET client library to interact with [Redmine](https://www.redmine.org)'s REST API. + + +## πŸš€ Features + +- Full REST API support with CRUD operations +- Supports both XML and JSON data formats +- Handles GZipped server responses transparently +- Easy integration via NuGet package +- Actively maintained and community-driven + +| Resource | Read | Create | Update | Delete | +|----------------------|:----:|:------:|:------:|:------:| +| Attachments | βœ… | βœ… | ❌ | ❌ | +| Custom Fields | βœ… | ❌ | ❌ | ❌ | +| Enumerations | βœ… | ❌ | ❌ | ❌ | +| Files | βœ… | βœ… | ❌ | ❌ | +| Groups | βœ… | βœ… | βœ… | βœ… | +| Issues | βœ… | βœ… | βœ… | βœ… | +| Issue Categories | βœ… | βœ… | βœ… | βœ… | +| Issue Relations | βœ… | βœ… | βœ… | βœ… | +| Issue Statuses | βœ… | ❌ | ❌ | ❌ | +| My Account | βœ… | ❌ | βœ… | ❌ | +| News | βœ… | βœ… | βœ… | βœ… | +| Projects | βœ… | βœ… | βœ… | βœ… | +| Project Memberships | βœ… | βœ… | βœ… | βœ… | +| Queries | βœ… | ❌ | ❌ | ❌ | +| Roles | βœ… | ❌ | ❌ | ❌ | +| Search | βœ… | | | | +| Time Entries | βœ… | βœ… | βœ… | βœ… | +| Trackers | βœ… | ❌ | ❌ | ❌ | +| Users | βœ… | βœ… | βœ… | βœ… | +| Versions | βœ… | βœ… | βœ… | βœ… | +| Wiki Pages | βœ… | βœ… | βœ… | βœ… | + + +## πŸ“¦ Installation + +Add the package via NuGet: + +```bash +dotnet add package Redmine.Net.Api +``` + +Or via Package Manager Console: + +```powershell +Install-Package Redmine.Net.Api +``` + + +## πŸ§‘β€πŸ’» Usage Example + +```csharp +using Redmine.Net.Api; +using Redmine.Net.Api.Types; +using System; +using System.Threading.Tasks; + +class Program +{ + static async Task Main() + { + var options = new RedmineManagerOptionsBuilder() + .WithHost("https://your-redmine-url/") + .WithApiKeyAuthentication("your-api-key"); + + var manager = new RedmineManager(options); + + // Retrieve an issue asynchronously + var issue = await manager.GetAsync(12345); + Console.WriteLine($"Issue subject: {issue.Subject}"); + } +} +``` +Explore more usage examples on the [Wiki](https://github.com/zapadi/redmine-net-api/wiki). + + +## πŸ“š Documentation + +Detailed API reference, guides, and tutorials are available in the [GitHub Wiki](https://github.com/zapadi/redmine-net-api/wiki). + + +## πŸ™Œ Contributing + +See the [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines. + +## πŸ’¬ Join on Slack + +Want to talk about Redmine integration, features, or contribute ideas? +Join Slack channel here: [dotnet-redmine](https://join.slack.com/t/dotnet-redmine/shared_invite/zt-36cvwm98j-10Sw3w4LITk1N6eqKKHWRw) + + +## 🀝 Contributors + +Thanks to all contributors! + + + + + + +## πŸ“ License + +This project is licensed under the [Apache License 2.0](LICENSE). + + +## β˜• Support + +If you find this project useful, consider ![[buying me a coffee](https://cdn.buymeacoffee.com/buttons/lato-yellow.png)](https://www.buymeacoffee.com/vXCNnz9) to support development. + diff --git a/appveyor.yml b/appveyor.yml old mode 100755 new mode 100644 index 07b8ab9e..8fc91681 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,61 +1,88 @@ -os: Visual Studio 2015 -version: 2.0.{build} -# environment: -# COVERALLS_REPO_TOKEN: -# secure: 8JYxwCWszeAaWBr41pD17LB925K7Sk7utvKsIb1qz44i2anf9uLmvh2q0ilMQTBO +version: '{build}' +image: + - Visual Studio 2022 + - Ubuntu + +environment: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + APPVEYOR_YML_DISABLE_PS_LINUX: false + BUILD_SUFFIX: "" + VERSION_SUFFIX: "" + +configuration: Release + pull_requests: - do_not_increment_build_number: false + do_not_increment_build_number: true + +nuget: + disable_publish_on_pr: true + branches: only: - - master -#configuration: Release -#platform: Any CPU -assembly_info: - patch: true - file: '**\AssemblyInfo.*' - assembly_version: '{version}' - assembly_file_version: '{version}' - assembly_informational_version: '{version}' + - master + - /\d*\.\d*\.\d*/ -build_script: - - Msbuild.exe redmine-net20-api/redmine-net20-api.csproj /verbosity:minimal /p:BuildNetFX20=true - - Msbuild.exe redmine-net40-api/redmine-net40-api.csproj /verbosity:minimal /p:BuildNetFX40=true - - Msbuild.exe redmine-net40-api-signed/redmine-net40-api-signed.csproj /verbosity:minimal /p:BuildNetFX40=true - - Msbuild.exe redmine-net45-api/redmine-net45-api.csproj /verbosity:minimal /p:BuildNetFX45=true - - Msbuild.exe redmine-net45-api-signed/redmine-net45-api-signed.csproj /verbosity:minimal /p:BuildNetFX45=true - - Msbuild.exe redmine-net451-api/redmine-net451-api.csproj /verbosity:minimal /p:BuildNetFX451=true - - Msbuild.exe redmine-net451-api-signed/redmine-net451-api-signed.csproj /verbosity:minimal /p:BuildNetFX451=true - - Msbuild.exe redmine-net452-api/redmine-net452-api.csproj /verbosity:minimal /p:BuildNetFX452=true - - Msbuild.exe redmine-net452-api-signed/redmine-net452-api-signed.csproj /verbosity:minimal /p:BuildNetFX452=true +init: + # Good practise, because Windows line endings are different from Unix/Linux ones + - ps: git config --global core.autocrlf true -before_build: -- nuget restore + - ps: $commitHash = $($env:APPVEYOR_REPO_COMMIT.substring(0,7)); + - ps: $branch = $env:APPVEYOR_REPO_BRANCH; + - ps: $buildNumber = $env:APPVEYOR_BUILD_NUMBER; + - ps: $isRepoTag = $env:APPVEYOR_REPO_TAG; + - ps: $revision = $(If ($isRepoTag -eq "true") {[string]::Empty} Else {"{0:00000}" -f [convert]::ToInt32("0" + $buildNumber, 10)}); + - ps: $suffix = $(If ([string]::IsNullOrEmpty($revision)) {[string]::Empty} Else {$branch.Substring(0, [math]::Min(10,$branch.Length))}); + - ps: $env:BUILD_SUFFIX = $(If ([string]::IsNullOrEmpty($suffix)) {"$branch-$commitHash"} Else {"$suffix-$commitHash"}); + - ps: $env:VERSION_SUFFIX = $(If ([string]::IsNullOrEmpty($suffix)) {[string]::Empty} Else {"--version-suffix=$suffix"}); -build: - project: redmine-net-api.sln - publish_nuget: true - verbosity: detailed +install: + - ps: dotnet restore redmine-net-api.sln + +before_build: + - ps: write-host "Is repo tag = $isRepoTag" -foregroundcolor Green + - ps: write-host "Build number = $buildNumber" -foregroundcolor Magenta + - ps: write-host "Branch = $branch" -foregroundcolor DarkYellow + - ps: write-host "Revision = $revision" -foregroundcolor Cyan + - ps: write-host "Build suffix = $env:BUILD_SUFFIX" -foregroundcolor Yellow + - ps: write-host "Version suffix = $env:VERSION_SUFFIX" -foregroundcolor Red + - ps: dotnet --version +build_script: + - ps: dotnet build src\redmine-net-api\redmine-net-api.csproj -c Release --version-suffix=$env:BUILD_SUFFIX + - ps: dotnet build src\redmine-net-api\redmine-net-api.csproj -c Release --version-suffix=$env:BUILD_SUFFIX -p:Sign=true + after_build: -- ps: nuget pack redmine-net-api.nuspec -Version $env:appveyor_build_version -- ps: nuget pack redmine-net-api-signed.nuspec -Version $env:appveyor_build_version - -nuget: - account_feed: true - project_feed: true + - ps: dotnet pack src\redmine-net-api\redmine-net-api.csproj -c Release --output .\artifacts --include-symbols -p:SymbolPackageFormat=snupkg --no-build $env:VERSION_SUFFIX + - ps: dotnet pack src\redmine-net-api\redmine-net-api.csproj -c Release --output .\artifacts --include-symbols -p:SymbolPackageFormat=snupkg --no-build $env:VERSION_SUFFIX -p:Sign=true test: off artifacts: -- path: '*.nupkg' - -# preserve "packages" directory in the root of build folder but will reset it if packages.config is modified -cache: - - '%USERPROFILE%\.nuget\packages -> **\project.json' # project.json cache - -deploy: -- provider: NuGet - api_key: - secure: PojoRMfsSOFs1hOuvSMRfR4+icliUzEg5hzjFjNUO03akBmKjWf22To/DYxSlzd/ - artifact: /.*\.nupkg/ - skip_symbols: true + - name: NuGet Packages + path: .\artifacts\**\*.nupkg + - name: NuGet Symbol Packages + path: .\artifacts\**\*.snupkg + +skip_commits: + files: + - '**/*.md' + - '**/*.gif' + - '**/*.png' + - LICENSE + - tests/* + +for: + - + matrix: + only: + - image: Ubuntu + + deploy: + - provider: NuGet + name: production + api_key: + secure: W38N2nYNrxoik84zDowE+ShuVYKUyPA/fl4/8nYMBEXwcG+pSHVkt/2r6xQvQOaC + skip_symbols: true + on: + APPVEYOR_REPO_TAG: true \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..4cb6caf7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,57 @@ +version: '3.7' + +services: + redmine: + ports: + - '8089:3000' + image: 'redmine:6.0.5-alpine' + container_name: 'redmine-web605' + depends_on: + - db-postgres + # healthcheck: + # test: ["CMD", "curl", "-f", "http://localhost:8089/"] + # interval: 1m30s + # timeout: 10s + # retries: 3 + # start_period: 40s + restart: unless-stopped + environment: + REDMINE_DB_POSTGRES: db-postgres + REDMINE_DB_PORT: 5432 + REDMINE_DB_DATABASE: redmine + REDMINE_DB_USERNAME: redmine-usr + REDMINE_DB_PASSWORD: redmine-pswd + networks: + - redmine-network + stop_grace_period: 30s + volumes: + - redmine-data:/usr/src/redmine/files + + db-postgres: + environment: + POSTGRES_DB: redmine + POSTGRES_USER: redmine-usr + POSTGRES_PASSWORD: redmine-pswd + container_name: 'redmine-db175' + image: 'postgres:17.5-alpine' + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 20s + timeout: 20s + retries: 5 + restart: unless-stopped + ports: + - '5432:5432' + volumes: + - postgres-data:/var/lib/postgresql/data + networks: + - redmine-network + stop_grace_period: 30s + +volumes: + postgres-data: + redmine-data: + +networks: + redmine-network: + driver: bridge \ No newline at end of file diff --git a/global.json b/global.json new file mode 100644 index 00000000..1f044567 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "9.0.203", + "allowPrerelease": false, + "rollForward": "latestMajor" + } +} \ No newline at end of file diff --git a/logo-resharper.gif b/logo-resharper.gif new file mode 100644 index 00000000..739dcb4d Binary files /dev/null and b/logo-resharper.gif differ diff --git a/logo.png b/logo.png old mode 100755 new mode 100644 index 0e88a5f4..83433adf Binary files a/logo.png and b/logo.png differ diff --git a/packages/repositories.config b/packages/repositories.config deleted file mode 100755 index cbea8908..00000000 --- a/packages/repositories.config +++ /dev/null @@ -1,5 +0,0 @@ -ο»Ώ - - - - \ No newline at end of file diff --git a/redmine-net-api-signed.nuspec b/redmine-net-api-signed.nuspec deleted file mode 100755 index d3710bf7..00000000 --- a/redmine-net-api-signed.nuspec +++ /dev/null @@ -1,35 +0,0 @@ - - - - redmine-api-signed - 0.0.0.0 - Redmine .NET API Signed - Adrian Popescu - Adrian Popescu - http://www.apache.org/licenses/LICENSE-2.0 - https://github.com/zapadi/redmine-net-api - https://github.com/zapadi/redmine-net-api/blob/master/logo.png - false - Redmine .NET API is a communication library for Redmine project management application. - - Copyright 2011 - 2017 - en-GB - Redmine API .NET Signed C# - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/redmine-net-api.nuspec b/redmine-net-api.nuspec deleted file mode 100755 index cfb8640c..00000000 --- a/redmine-net-api.nuspec +++ /dev/null @@ -1,37 +0,0 @@ - - - - redmine-api - 0.0.0.0 - Redmine .NET API - Adrian Popescu - Adrian Popescu - http://www.apache.org/licenses/LICENSE-2.0 - https://github.com/zapadi/redmine-net-api - https://github.com/zapadi/redmine-net-api/blob/master/logo.png - false - Redmine .NET API is a communication library for Redmine project management application. - - Copyright 2011 - 2017 - en-GB - Redmine API .NET C# - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/redmine-net-api.sln b/redmine-net-api.sln index d2da88b0..6e9f665f 100644 --- a/redmine-net-api.sln +++ b/redmine-net-api.sln @@ -1,270 +1,101 @@ -ο»Ώ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26228.9 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29503.13 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "redmine-net20-api", "redmine-net20-api\redmine-net20-api.csproj", "{DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0DFF4758-5C19-4D8F-BA6C-76E618323F6A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "redmine-net40-api", "redmine-net40-api\redmine-net40-api.csproj", "{0D9B763C-A16B-463B-BDDD-0A0467DCD32E}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F3F4278D-6271-4F77-BA88-41555D53CBD1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "redmine-net45-api", "redmine-net45-api\redmine-net45-api.csproj", "{89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "redmine-net-api", "src\redmine-net-api\redmine-net-api.csproj", "{0E6B9B72-445D-4E71-8D29-48C4A009AB03}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "redmine-net45-api-signed", "redmine-net45-api-signed\redmine-net45-api-signed.csproj", "{82796546-0F57-425B-BB77-751FA24D49D5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "redmine-net-api.Tests", "tests\redmine-net-api.Tests\redmine-net-api.Tests.csproj", "{900EF0B3-0233-45DA-811F-4C59483E8452}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "redmine-net40-api-signed", "redmine-net40-api-signed\redmine-net40-api-signed.csproj", "{1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionFolder", "SolutionFolder", "{E8C35EC2-DD90-46E8-9B63-84EFD5F2FDE3}" + ProjectSection(SolutionItems) = preProject + CHANGELOG.md = CHANGELOG.md + CONTRIBUTING.md = CONTRIBUTING.md + ISSUE_TEMPLATE.md = ISSUE_TEMPLATE.md + LICENSE = LICENSE + PULL_REQUEST_TEMPLATE.md = PULL_REQUEST_TEMPLATE.md + README.md = README.md + EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "redmine-net451-api", "redmine-net451-api\redmine-net451-api.csproj", "{4AB94C09-8CFB-41C6-87D1-6B972E7F9307}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitActions", "GitActions", "{79119F8B-C468-4DC8-BE6F-6E7102BD2079}" + ProjectSection(SolutionItems) = preProject + .github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml + .github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml + .github\workflows\publish.yml = .github\workflows\publish.yml + EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "redmine-net451-api-signed", "redmine-net451-api-signed\redmine-net451-api-signed.csproj", "{6E6E642E-F35A-4EA7-A2D7-16725156BD33}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AppVeyor", "AppVeyor", "{F20AEA6C-B957-4A83-9616-B91548B4C561}" + ProjectSection(SolutionItems) = preProject + appveyor.yml = appveyor.yml + EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "xUnitTest-redmine-net-api", "xUnitTest-redmine-net45-api\xUnitTest-redmine-net-api.csproj", "{170210BF-5F03-4531-8A63-06E356CA284B}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{707B6A3F-1A2C-4EFE-851F-1DB0E68CFFFB}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + releasenotes.props = releasenotes.props + signing.props = signing.props + version.props = version.props + Directory.Packages.props = Directory.Packages.props + EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "redmine-net452-api", "redmine-net452-api\redmine-net452-api.csproj", "{404B264F-363B-44AD-AE8D-2587C2E6FA82}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docker", "Docker", "{1D340EEB-C535-45D4-80D7-ADD4434D7B77}" + ProjectSection(SolutionItems) = preProject + docker-compose.yml = docker-compose.yml + EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "redmine-net452-api-signed", "redmine-net452-api-signed\redmine-net452-api-signed.csproj", "{0AE35DA8-1D10-4FA4-8CF7-D1EC18A42FB7}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Others", "Others", "{4ADECA2A-4D7B-4F05-85A2-0C0963A83689}" + ProjectSection(SolutionItems) = preProject + logo.png = logo.png + redmine-net-api.snk = redmine-net-api.snk + global.json = global.json + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "redmine-net-api.Integration.Tests", "tests\redmine-net-api.Integration.Tests\redmine-net-api.Integration.Tests.csproj", "{254DABFE-7C92-4C16-84A5-630330D56D4D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU - Debug|Mixed Platforms = Debug|Mixed Platforms - Debug|x86 = Debug|x86 - DebugJSON|Any CPU = DebugJSON|Any CPU - DebugJSON|Mixed Platforms = DebugJSON|Mixed Platforms - DebugJSON|x86 = DebugJSON|x86 - DebugXML|Any CPU = DebugXML|Any CPU - DebugXML|Mixed Platforms = DebugXML|Mixed Platforms - DebugXML|x86 = DebugXML|x86 + DebugJson|Any CPU = DebugJson|Any CPU Release|Any CPU = Release|Any CPU - Release|Mixed Platforms = Release|Mixed Platforms - Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.Debug|x86.ActiveCfg = Debug|Any CPU - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.DebugJSON|Any CPU.ActiveCfg = DebugJSON|Any CPU - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.DebugJSON|Any CPU.Build.0 = DebugJSON|Any CPU - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.DebugJSON|Mixed Platforms.ActiveCfg = DebugJSON|Any CPU - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.DebugJSON|Mixed Platforms.Build.0 = DebugJSON|Any CPU - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.DebugJSON|x86.ActiveCfg = DebugJSON|Any CPU - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.DebugJSON|x86.Build.0 = DebugJSON|Any CPU - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.DebugXML|Any CPU.ActiveCfg = DebugXML|Any CPU - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.DebugXML|Any CPU.Build.0 = DebugXML|Any CPU - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.DebugXML|Mixed Platforms.ActiveCfg = DebugXML|Any CPU - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.DebugXML|Mixed Platforms.Build.0 = DebugXML|Any CPU - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.DebugXML|x86.ActiveCfg = DebugXML|Any CPU - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.DebugXML|x86.Build.0 = DebugXML|Any CPU - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.Release|Any CPU.Build.0 = Release|Any CPU - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.Release|x86.ActiveCfg = Release|Any CPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.Debug|x86.ActiveCfg = Debug|Any CPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.DebugJSON|Any CPU.ActiveCfg = DebugJSON|Any CPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.DebugJSON|Any CPU.Build.0 = DebugJSON|Any CPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.DebugJSON|Mixed Platforms.ActiveCfg = DebugJSON|Any CPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.DebugJSON|Mixed Platforms.Build.0 = DebugJSON|Any CPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.DebugJSON|x86.ActiveCfg = DebugJSON|Any CPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.DebugJSON|x86.Build.0 = DebugJSON|Any CPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.DebugXML|Any CPU.ActiveCfg = DebugXML|Any CPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.DebugXML|Any CPU.Build.0 = DebugXML|Any CPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.DebugXML|Mixed Platforms.ActiveCfg = DebugXML|Any CPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.DebugXML|Mixed Platforms.Build.0 = DebugXML|Any CPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.DebugXML|x86.ActiveCfg = DebugXML|Any CPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.DebugXML|x86.Build.0 = DebugXML|Any CPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.Release|Any CPU.Build.0 = Release|Any CPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.Release|x86.ActiveCfg = Release|Any CPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.Debug|x86.ActiveCfg = Debug|Any CPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.DebugJSON|Any CPU.ActiveCfg = DebugJSON|Any CPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.DebugJSON|Any CPU.Build.0 = DebugJSON|Any CPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.DebugJSON|Mixed Platforms.ActiveCfg = DebugJSON|Any CPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.DebugJSON|Mixed Platforms.Build.0 = DebugJSON|Any CPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.DebugJSON|x86.ActiveCfg = DebugJSON|Any CPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.DebugJSON|x86.Build.0 = DebugJSON|Any CPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.DebugXML|Any CPU.ActiveCfg = DebugXML|Any CPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.DebugXML|Any CPU.Build.0 = DebugXML|Any CPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.DebugXML|Mixed Platforms.ActiveCfg = DebugXML|Any CPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.DebugXML|Mixed Platforms.Build.0 = DebugXML|Any CPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.DebugXML|x86.ActiveCfg = DebugXML|Any CPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.DebugXML|x86.Build.0 = DebugXML|Any CPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.Release|Any CPU.Build.0 = Release|Any CPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.Release|x86.ActiveCfg = Release|Any CPU - {82796546-0F57-425B-BB77-751FA24D49D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {82796546-0F57-425B-BB77-751FA24D49D5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {82796546-0F57-425B-BB77-751FA24D49D5}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {82796546-0F57-425B-BB77-751FA24D49D5}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {82796546-0F57-425B-BB77-751FA24D49D5}.Debug|x86.ActiveCfg = Debug|Any CPU - {82796546-0F57-425B-BB77-751FA24D49D5}.DebugJSON|Any CPU.ActiveCfg = DebugJSON|Any CPU - {82796546-0F57-425B-BB77-751FA24D49D5}.DebugJSON|Any CPU.Build.0 = DebugJSON|Any CPU - {82796546-0F57-425B-BB77-751FA24D49D5}.DebugJSON|Mixed Platforms.ActiveCfg = DebugJSON|Any CPU - {82796546-0F57-425B-BB77-751FA24D49D5}.DebugJSON|Mixed Platforms.Build.0 = DebugJSON|Any CPU - {82796546-0F57-425B-BB77-751FA24D49D5}.DebugJSON|x86.ActiveCfg = DebugJSON|Any CPU - {82796546-0F57-425B-BB77-751FA24D49D5}.DebugJSON|x86.Build.0 = DebugJSON|Any CPU - {82796546-0F57-425B-BB77-751FA24D49D5}.DebugXML|Any CPU.ActiveCfg = DebugXML|Any CPU - {82796546-0F57-425B-BB77-751FA24D49D5}.DebugXML|Any CPU.Build.0 = DebugXML|Any CPU - {82796546-0F57-425B-BB77-751FA24D49D5}.DebugXML|Mixed Platforms.ActiveCfg = DebugXML|Any CPU - {82796546-0F57-425B-BB77-751FA24D49D5}.DebugXML|Mixed Platforms.Build.0 = DebugXML|Any CPU - {82796546-0F57-425B-BB77-751FA24D49D5}.DebugXML|x86.ActiveCfg = DebugXML|Any CPU - {82796546-0F57-425B-BB77-751FA24D49D5}.DebugXML|x86.Build.0 = DebugXML|Any CPU - {82796546-0F57-425B-BB77-751FA24D49D5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {82796546-0F57-425B-BB77-751FA24D49D5}.Release|Any CPU.Build.0 = Release|Any CPU - {82796546-0F57-425B-BB77-751FA24D49D5}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {82796546-0F57-425B-BB77-751FA24D49D5}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {82796546-0F57-425B-BB77-751FA24D49D5}.Release|x86.ActiveCfg = Release|Any CPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.Debug|x86.ActiveCfg = Debug|Any CPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.DebugJSON|Any CPU.ActiveCfg = DebugJSON|Any CPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.DebugJSON|Any CPU.Build.0 = DebugJSON|Any CPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.DebugJSON|Mixed Platforms.ActiveCfg = DebugJSON|Any CPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.DebugJSON|Mixed Platforms.Build.0 = DebugJSON|Any CPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.DebugJSON|x86.ActiveCfg = DebugJSON|Any CPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.DebugJSON|x86.Build.0 = DebugJSON|Any CPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.DebugXML|Any CPU.ActiveCfg = DebugXML|Any CPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.DebugXML|Any CPU.Build.0 = DebugXML|Any CPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.DebugXML|Mixed Platforms.ActiveCfg = DebugXML|Any CPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.DebugXML|Mixed Platforms.Build.0 = DebugXML|Any CPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.DebugXML|x86.ActiveCfg = DebugXML|Any CPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.DebugXML|x86.Build.0 = DebugXML|Any CPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.Release|Any CPU.Build.0 = Release|Any CPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.Release|x86.ActiveCfg = Release|Any CPU - {4AB94C09-8CFB-41C6-87D1-6B972E7F9307}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4AB94C09-8CFB-41C6-87D1-6B972E7F9307}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4AB94C09-8CFB-41C6-87D1-6B972E7F9307}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {4AB94C09-8CFB-41C6-87D1-6B972E7F9307}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {4AB94C09-8CFB-41C6-87D1-6B972E7F9307}.Debug|x86.ActiveCfg = Debug|Any CPU - {4AB94C09-8CFB-41C6-87D1-6B972E7F9307}.DebugJSON|Any CPU.ActiveCfg = Debug|Any CPU - {4AB94C09-8CFB-41C6-87D1-6B972E7F9307}.DebugJSON|Any CPU.Build.0 = Debug|Any CPU - {4AB94C09-8CFB-41C6-87D1-6B972E7F9307}.DebugJSON|Mixed Platforms.ActiveCfg = Debug|Any CPU - {4AB94C09-8CFB-41C6-87D1-6B972E7F9307}.DebugJSON|Mixed Platforms.Build.0 = Debug|Any CPU - {4AB94C09-8CFB-41C6-87D1-6B972E7F9307}.DebugJSON|x86.ActiveCfg = Debug|Any CPU - {4AB94C09-8CFB-41C6-87D1-6B972E7F9307}.DebugXML|Any CPU.ActiveCfg = Debug|Any CPU - {4AB94C09-8CFB-41C6-87D1-6B972E7F9307}.DebugXML|Any CPU.Build.0 = Debug|Any CPU - {4AB94C09-8CFB-41C6-87D1-6B972E7F9307}.DebugXML|Mixed Platforms.ActiveCfg = Debug|Any CPU - {4AB94C09-8CFB-41C6-87D1-6B972E7F9307}.DebugXML|Mixed Platforms.Build.0 = Debug|Any CPU - {4AB94C09-8CFB-41C6-87D1-6B972E7F9307}.DebugXML|x86.ActiveCfg = Debug|Any CPU - {4AB94C09-8CFB-41C6-87D1-6B972E7F9307}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4AB94C09-8CFB-41C6-87D1-6B972E7F9307}.Release|Any CPU.Build.0 = Release|Any CPU - {4AB94C09-8CFB-41C6-87D1-6B972E7F9307}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {4AB94C09-8CFB-41C6-87D1-6B972E7F9307}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {4AB94C09-8CFB-41C6-87D1-6B972E7F9307}.Release|x86.ActiveCfg = Release|Any CPU - {6E6E642E-F35A-4EA7-A2D7-16725156BD33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6E6E642E-F35A-4EA7-A2D7-16725156BD33}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6E6E642E-F35A-4EA7-A2D7-16725156BD33}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {6E6E642E-F35A-4EA7-A2D7-16725156BD33}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {6E6E642E-F35A-4EA7-A2D7-16725156BD33}.Debug|x86.ActiveCfg = Debug|Any CPU - {6E6E642E-F35A-4EA7-A2D7-16725156BD33}.DebugJSON|Any CPU.ActiveCfg = Debug|Any CPU - {6E6E642E-F35A-4EA7-A2D7-16725156BD33}.DebugJSON|Any CPU.Build.0 = Debug|Any CPU - {6E6E642E-F35A-4EA7-A2D7-16725156BD33}.DebugJSON|Mixed Platforms.ActiveCfg = Debug|Any CPU - {6E6E642E-F35A-4EA7-A2D7-16725156BD33}.DebugJSON|Mixed Platforms.Build.0 = Debug|Any CPU - {6E6E642E-F35A-4EA7-A2D7-16725156BD33}.DebugJSON|x86.ActiveCfg = Debug|Any CPU - {6E6E642E-F35A-4EA7-A2D7-16725156BD33}.DebugXML|Any CPU.ActiveCfg = Debug|Any CPU - {6E6E642E-F35A-4EA7-A2D7-16725156BD33}.DebugXML|Any CPU.Build.0 = Debug|Any CPU - {6E6E642E-F35A-4EA7-A2D7-16725156BD33}.DebugXML|Mixed Platforms.ActiveCfg = Debug|Any CPU - {6E6E642E-F35A-4EA7-A2D7-16725156BD33}.DebugXML|Mixed Platforms.Build.0 = Debug|Any CPU - {6E6E642E-F35A-4EA7-A2D7-16725156BD33}.DebugXML|x86.ActiveCfg = Debug|Any CPU - {6E6E642E-F35A-4EA7-A2D7-16725156BD33}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6E6E642E-F35A-4EA7-A2D7-16725156BD33}.Release|Any CPU.Build.0 = Release|Any CPU - {6E6E642E-F35A-4EA7-A2D7-16725156BD33}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {6E6E642E-F35A-4EA7-A2D7-16725156BD33}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {6E6E642E-F35A-4EA7-A2D7-16725156BD33}.Release|x86.ActiveCfg = Release|Any CPU - {170210BF-5F03-4531-8A63-06E356CA284B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {170210BF-5F03-4531-8A63-06E356CA284B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {170210BF-5F03-4531-8A63-06E356CA284B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {170210BF-5F03-4531-8A63-06E356CA284B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {170210BF-5F03-4531-8A63-06E356CA284B}.Debug|x86.ActiveCfg = Debug|Any CPU - {170210BF-5F03-4531-8A63-06E356CA284B}.Debug|x86.Build.0 = Debug|Any CPU - {170210BF-5F03-4531-8A63-06E356CA284B}.DebugJSON|Any CPU.ActiveCfg = Debug|Any CPU - {170210BF-5F03-4531-8A63-06E356CA284B}.DebugJSON|Any CPU.Build.0 = Debug|Any CPU - {170210BF-5F03-4531-8A63-06E356CA284B}.DebugJSON|Mixed Platforms.ActiveCfg = Debug|Any CPU - {170210BF-5F03-4531-8A63-06E356CA284B}.DebugJSON|Mixed Platforms.Build.0 = Debug|Any CPU - {170210BF-5F03-4531-8A63-06E356CA284B}.DebugJSON|x86.ActiveCfg = Debug|Any CPU - {170210BF-5F03-4531-8A63-06E356CA284B}.DebugJSON|x86.Build.0 = Debug|Any CPU - {170210BF-5F03-4531-8A63-06E356CA284B}.DebugXML|Any CPU.ActiveCfg = Debug|Any CPU - {170210BF-5F03-4531-8A63-06E356CA284B}.DebugXML|Any CPU.Build.0 = Debug|Any CPU - {170210BF-5F03-4531-8A63-06E356CA284B}.DebugXML|Mixed Platforms.ActiveCfg = Debug|Any CPU - {170210BF-5F03-4531-8A63-06E356CA284B}.DebugXML|Mixed Platforms.Build.0 = Debug|Any CPU - {170210BF-5F03-4531-8A63-06E356CA284B}.DebugXML|x86.ActiveCfg = Debug|Any CPU - {170210BF-5F03-4531-8A63-06E356CA284B}.DebugXML|x86.Build.0 = Debug|Any CPU - {170210BF-5F03-4531-8A63-06E356CA284B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {170210BF-5F03-4531-8A63-06E356CA284B}.Release|Any CPU.Build.0 = Release|Any CPU - {170210BF-5F03-4531-8A63-06E356CA284B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {170210BF-5F03-4531-8A63-06E356CA284B}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {170210BF-5F03-4531-8A63-06E356CA284B}.Release|x86.ActiveCfg = Release|Any CPU - {170210BF-5F03-4531-8A63-06E356CA284B}.Release|x86.Build.0 = Release|Any CPU - {404B264F-363B-44AD-AE8D-2587C2E6FA82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {404B264F-363B-44AD-AE8D-2587C2E6FA82}.Debug|Any CPU.Build.0 = Debug|Any CPU - {404B264F-363B-44AD-AE8D-2587C2E6FA82}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {404B264F-363B-44AD-AE8D-2587C2E6FA82}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {404B264F-363B-44AD-AE8D-2587C2E6FA82}.Debug|x86.ActiveCfg = Debug|Any CPU - {404B264F-363B-44AD-AE8D-2587C2E6FA82}.Debug|x86.Build.0 = Debug|Any CPU - {404B264F-363B-44AD-AE8D-2587C2E6FA82}.DebugJSON|Any CPU.ActiveCfg = Debug|Any CPU - {404B264F-363B-44AD-AE8D-2587C2E6FA82}.DebugJSON|Any CPU.Build.0 = Debug|Any CPU - {404B264F-363B-44AD-AE8D-2587C2E6FA82}.DebugJSON|Mixed Platforms.ActiveCfg = Debug|Any CPU - {404B264F-363B-44AD-AE8D-2587C2E6FA82}.DebugJSON|Mixed Platforms.Build.0 = Debug|Any CPU - {404B264F-363B-44AD-AE8D-2587C2E6FA82}.DebugJSON|x86.ActiveCfg = Debug|Any CPU - {404B264F-363B-44AD-AE8D-2587C2E6FA82}.DebugJSON|x86.Build.0 = Debug|Any CPU - {404B264F-363B-44AD-AE8D-2587C2E6FA82}.DebugXML|Any CPU.ActiveCfg = Debug|Any CPU - {404B264F-363B-44AD-AE8D-2587C2E6FA82}.DebugXML|Any CPU.Build.0 = Debug|Any CPU - {404B264F-363B-44AD-AE8D-2587C2E6FA82}.DebugXML|Mixed Platforms.ActiveCfg = Debug|Any CPU - {404B264F-363B-44AD-AE8D-2587C2E6FA82}.DebugXML|Mixed Platforms.Build.0 = Debug|Any CPU - {404B264F-363B-44AD-AE8D-2587C2E6FA82}.DebugXML|x86.ActiveCfg = Debug|Any CPU - {404B264F-363B-44AD-AE8D-2587C2E6FA82}.DebugXML|x86.Build.0 = Debug|Any CPU - {404B264F-363B-44AD-AE8D-2587C2E6FA82}.Release|Any CPU.ActiveCfg = Release|Any CPU - {404B264F-363B-44AD-AE8D-2587C2E6FA82}.Release|Any CPU.Build.0 = Release|Any CPU - {404B264F-363B-44AD-AE8D-2587C2E6FA82}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {404B264F-363B-44AD-AE8D-2587C2E6FA82}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {404B264F-363B-44AD-AE8D-2587C2E6FA82}.Release|x86.ActiveCfg = Release|Any CPU - {404B264F-363B-44AD-AE8D-2587C2E6FA82}.Release|x86.Build.0 = Release|Any CPU - {0AE35DA8-1D10-4FA4-8CF7-D1EC18A42FB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0AE35DA8-1D10-4FA4-8CF7-D1EC18A42FB7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0AE35DA8-1D10-4FA4-8CF7-D1EC18A42FB7}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {0AE35DA8-1D10-4FA4-8CF7-D1EC18A42FB7}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {0AE35DA8-1D10-4FA4-8CF7-D1EC18A42FB7}.Debug|x86.ActiveCfg = Debug|Any CPU - {0AE35DA8-1D10-4FA4-8CF7-D1EC18A42FB7}.Debug|x86.Build.0 = Debug|Any CPU - {0AE35DA8-1D10-4FA4-8CF7-D1EC18A42FB7}.DebugJSON|Any CPU.ActiveCfg = Debug|Any CPU - {0AE35DA8-1D10-4FA4-8CF7-D1EC18A42FB7}.DebugJSON|Any CPU.Build.0 = Debug|Any CPU - {0AE35DA8-1D10-4FA4-8CF7-D1EC18A42FB7}.DebugJSON|Mixed Platforms.ActiveCfg = Debug|Any CPU - {0AE35DA8-1D10-4FA4-8CF7-D1EC18A42FB7}.DebugJSON|Mixed Platforms.Build.0 = Debug|Any CPU - {0AE35DA8-1D10-4FA4-8CF7-D1EC18A42FB7}.DebugJSON|x86.ActiveCfg = Debug|Any CPU - {0AE35DA8-1D10-4FA4-8CF7-D1EC18A42FB7}.DebugJSON|x86.Build.0 = Debug|Any CPU - {0AE35DA8-1D10-4FA4-8CF7-D1EC18A42FB7}.DebugXML|Any CPU.ActiveCfg = Debug|Any CPU - {0AE35DA8-1D10-4FA4-8CF7-D1EC18A42FB7}.DebugXML|Any CPU.Build.0 = Debug|Any CPU - {0AE35DA8-1D10-4FA4-8CF7-D1EC18A42FB7}.DebugXML|Mixed Platforms.ActiveCfg = Debug|Any CPU - {0AE35DA8-1D10-4FA4-8CF7-D1EC18A42FB7}.DebugXML|Mixed Platforms.Build.0 = Debug|Any CPU - {0AE35DA8-1D10-4FA4-8CF7-D1EC18A42FB7}.DebugXML|x86.ActiveCfg = Debug|Any CPU - {0AE35DA8-1D10-4FA4-8CF7-D1EC18A42FB7}.DebugXML|x86.Build.0 = Debug|Any CPU - {0AE35DA8-1D10-4FA4-8CF7-D1EC18A42FB7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0AE35DA8-1D10-4FA4-8CF7-D1EC18A42FB7}.Release|Any CPU.Build.0 = Release|Any CPU - {0AE35DA8-1D10-4FA4-8CF7-D1EC18A42FB7}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {0AE35DA8-1D10-4FA4-8CF7-D1EC18A42FB7}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {0AE35DA8-1D10-4FA4-8CF7-D1EC18A42FB7}.Release|x86.ActiveCfg = Release|Any CPU - {0AE35DA8-1D10-4FA4-8CF7-D1EC18A42FB7}.Release|x86.Build.0 = Release|Any CPU + {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.DebugJson|Any CPU.ActiveCfg = DebugJson|Any CPU + {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.DebugJson|Any CPU.Build.0 = DebugJson|Any CPU + {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.Release|Any CPU.Build.0 = Debug|Any CPU + {900EF0B3-0233-45DA-811F-4C59483E8452}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {900EF0B3-0233-45DA-811F-4C59483E8452}.Debug|Any CPU.Build.0 = Debug|Any CPU + {900EF0B3-0233-45DA-811F-4C59483E8452}.DebugJson|Any CPU.ActiveCfg = DebugJson|Any CPU + {900EF0B3-0233-45DA-811F-4C59483E8452}.DebugJson|Any CPU.Build.0 = DebugJson|Any CPU + {900EF0B3-0233-45DA-811F-4C59483E8452}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {254DABFE-7C92-4C16-84A5-630330D56D4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {254DABFE-7C92-4C16-84A5-630330D56D4D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {254DABFE-7C92-4C16-84A5-630330D56D4D}.DebugJson|Any CPU.ActiveCfg = Debug|Any CPU + {254DABFE-7C92-4C16-84A5-630330D56D4D}.DebugJson|Any CPU.Build.0 = Debug|Any CPU + {254DABFE-7C92-4C16-84A5-630330D56D4D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {254DABFE-7C92-4C16-84A5-630330D56D4D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {0E6B9B72-445D-4E71-8D29-48C4A009AB03} = {0DFF4758-5C19-4D8F-BA6C-76E618323F6A} + {900EF0B3-0233-45DA-811F-4C59483E8452} = {F3F4278D-6271-4F77-BA88-41555D53CBD1} + {79119F8B-C468-4DC8-BE6F-6E7102BD2079} = {E8C35EC2-DD90-46E8-9B63-84EFD5F2FDE3} + {F20AEA6C-B957-4A83-9616-B91548B4C561} = {E8C35EC2-DD90-46E8-9B63-84EFD5F2FDE3} + {707B6A3F-1A2C-4EFE-851F-1DB0E68CFFFB} = {E8C35EC2-DD90-46E8-9B63-84EFD5F2FDE3} + {1D340EEB-C535-45D4-80D7-ADD4434D7B77} = {E8C35EC2-DD90-46E8-9B63-84EFD5F2FDE3} + {4ADECA2A-4D7B-4F05-85A2-0C0963A83689} = {E8C35EC2-DD90-46E8-9B63-84EFD5F2FDE3} + {254DABFE-7C92-4C16-84A5-630330D56D4D} = {F3F4278D-6271-4F77-BA88-41555D53CBD1} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {4AA87D90-ABD0-4793-BE47-955B35FAE2BB} + EndGlobalSection GlobalSection(CodealikeProperties) = postSolution SolutionGuid = 74da85cc-5a0d-4590-a976-666d0b2d41cb EndGlobalSection diff --git a/redmine-net-api.sln.DotSettings b/redmine-net-api.sln.DotSettings deleted file mode 100755 index 372a1f5b..00000000 --- a/redmine-net-api.sln.DotSettings +++ /dev/null @@ -1,3 +0,0 @@ -ο»Ώ - JSON - XML \ No newline at end of file diff --git a/redmine-net40-api-signed/redmine-net-api.snk b/redmine-net-api.snk old mode 100755 new mode 100644 similarity index 100% rename from redmine-net40-api-signed/redmine-net-api.snk rename to redmine-net-api.snk diff --git a/redmine-net20-api/Async/RedmineManagerAsync.cs b/redmine-net20-api/Async/RedmineManagerAsync.cs deleted file mode 100755 index 40154158..00000000 --- a/redmine-net20-api/Async/RedmineManagerAsync.cs +++ /dev/null @@ -1,255 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Specialized; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.Async -{ - /// - /// - /// - public delegate void Task(); - - /// - /// - /// - /// The type of the resource. - /// - public delegate TRes Task(); - - /// - /// - /// - public static class RedmineManagerAsync - { - /// - /// Gets the current user asynchronous. - /// - /// The redmine manager. - /// The parameters. - /// - public static Task GetCurrentUserAsync(this RedmineManager redmineManager, - NameValueCollection parameters = null) - { - return delegate { return redmineManager.GetCurrentUser(parameters); }; - } - - /// - /// Creates the or update wiki page asynchronous. - /// - /// The redmine manager. - /// The project identifier. - /// Name of the page. - /// The wiki page. - /// - public static Task CreateOrUpdateWikiPageAsync(this RedmineManager redmineManager, string projectId, - string pageName, WikiPage wikiPage) - { - return delegate { return redmineManager.CreateOrUpdateWikiPage(projectId, pageName, wikiPage); }; - } - - /// - /// Deletes the wiki page asynchronous. - /// - /// The redmine manager. - /// The project identifier. - /// Name of the page. - /// - public static Task DeleteWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName) - { - return delegate { redmineManager.DeleteWikiPage(projectId, pageName); }; - } - - /// - /// Gets the wiki page asynchronous. - /// - /// The redmine manager. - /// The project identifier. - /// The parameters. - /// Name of the page. - /// The version. - /// - public static Task GetWikiPageAsync(this RedmineManager redmineManager, string projectId, - NameValueCollection parameters, string pageName, uint version = 0) - { - return delegate { return redmineManager.GetWikiPage(projectId, parameters, pageName, version); }; - } - - /// - /// Gets all wiki pages asynchronous. - /// - /// The redmine manager. - /// The parameters. - /// The project identifier. - /// - public static Task> GetAllWikiPagesAsync(this RedmineManager redmineManager, - NameValueCollection parameters, string projectId) - { - return delegate { return redmineManager.GetAllWikiPages(projectId); }; - } - - /// - /// Adds the user to group asynchronous. - /// - /// The redmine manager. - /// The group identifier. - /// The user identifier. - /// - public static Task AddUserToGroupAsync(this RedmineManager redmineManager, int groupId, int userId) - { - return delegate { redmineManager.AddUserToGroup(groupId, userId); }; - } - - /// - /// Removes the user from group asynchronous. - /// - /// The redmine manager. - /// The group identifier. - /// The user identifier. - /// - public static Task RemoveUserFromGroupAsync(this RedmineManager redmineManager, int groupId, int userId) - { - return delegate { redmineManager.RemoveUserFromGroup(groupId, userId); }; - } - - /// - /// Adds the watcher to issue asynchronous. - /// - /// The redmine manager. - /// The issue identifier. - /// The user identifier. - /// - public static Task AddWatcherToIssueAsync(this RedmineManager redmineManager, int issueId, int userId) - { - return delegate { redmineManager.AddWatcherToIssue(issueId, userId); }; - } - - /// - /// Removes the watcher from issue asynchronous. - /// - /// The redmine manager. - /// The issue identifier. - /// The user identifier. - /// - public static Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManager, int issueId, int userId) - { - return delegate { redmineManager.RemoveWatcherFromIssue(issueId, userId); }; - } - - /// - /// Gets the object asynchronous. - /// - /// - /// The redmine manager. - /// The identifier. - /// The parameters. - /// - public static Task GetObjectAsync(this RedmineManager redmineManager, string id, - NameValueCollection parameters) where T : class, new() - { - return delegate { return redmineManager.GetObject(id, parameters); }; - } - - /// - /// Creates the object asynchronous. - /// - /// - /// The redmine manager. - /// The object. - /// - public static Task CreateObjectAsync(this RedmineManager redmineManager, T obj) where T : class, new() - { - return CreateObjectAsync(redmineManager, obj, null); - } - - /// - /// Creates the object asynchronous. - /// - /// - /// The redmine manager. - /// The object. - /// The owner identifier. - /// - public static Task CreateObjectAsync(this RedmineManager redmineManager, T obj, string ownerId) - where T : class, new() - { - return delegate { return redmineManager.CreateObject(obj, ownerId); }; - } - - /// - /// Gets the paginated objects asynchronous. - /// - /// - /// The redmine manager. - /// The parameters. - /// - public static Task> GetPaginatedObjectsAsync(this RedmineManager redmineManager, - NameValueCollection parameters) where T : class, new() - { - return delegate { return redmineManager.GetPaginatedObjects(parameters); }; - } - - /// - /// Gets the objects asynchronous. - /// - /// - /// The redmine manager. - /// The parameters. - /// - public static Task> GetObjectsAsync(this RedmineManager redmineManager, - NameValueCollection parameters) where T : class, new() - { - return delegate { return redmineManager.GetObjects(parameters); }; - } - - /// - /// Updates the object asynchronous. - /// - /// - /// The redmine manager. - /// The identifier. - /// The object. - /// The project identifier. - /// - public static Task UpdateObjectAsync(this RedmineManager redmineManager, string id, T obj, - string projectId = null) where T : class, new() - { - return delegate { redmineManager.UpdateObject(id, obj, projectId); }; - } - - /// - /// Deletes the object asynchronous. - /// - /// - /// The redmine manager. - /// The identifier. - /// The parameters. - /// - public static Task DeleteObjectAsync(this RedmineManager redmineManager, string id, - NameValueCollection parameters) where T : class, new() - { - return delegate { redmineManager.DeleteObject(id); }; - } - - /// - /// Uploads the file asynchronous. - /// - /// The redmine manager. - /// The data. - /// - public static Task UploadFileAsync(this RedmineManager redmineManager, byte[] data) - { - return delegate { return redmineManager.UploadFile(data); }; - } - - /// - /// Downloads the file asynchronous. - /// - /// The redmine manager. - /// The address. - /// - public static Task DownloadFileAsync(this RedmineManager redmineManager, string address) - { - return delegate { return redmineManager.DownloadFile(address); }; - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Exceptions/ConflictException.cs b/redmine-net20-api/Exceptions/ConflictException.cs deleted file mode 100755 index 1f1ee1f2..00000000 --- a/redmine-net20-api/Exceptions/ConflictException.cs +++ /dev/null @@ -1,84 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Runtime.Serialization; - -namespace Redmine.Net.Api.Exceptions -{ - /// - /// - /// - public class ConflictException : RedmineException - { - /// - /// Initializes a new instance of the class. - /// - public ConflictException() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - public ConflictException(string message) - : base(message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public ConflictException(string format, params object[] args) - : base(string.Format(format, args)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public ConflictException(string message, Exception innerException) - : base(message, innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - public ConflictException(string format, Exception innerException, params object[] args) - : base(string.Format(format, args), innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - protected ConflictException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Exceptions/ForbiddenException.cs b/redmine-net20-api/Exceptions/ForbiddenException.cs deleted file mode 100644 index 97f6fb77..00000000 --- a/redmine-net20-api/Exceptions/ForbiddenException.cs +++ /dev/null @@ -1,84 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Runtime.Serialization; - -namespace Redmine.Net.Api.Exceptions -{ - /// - /// - /// - public class ForbiddenException : RedmineException - { - /// - /// Initializes a new instance of the class. - /// - public ForbiddenException() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - public ForbiddenException(string message) - : base(message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public ForbiddenException(string format, params object[] args) - : base(string.Format(format, args)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public ForbiddenException(string message, Exception innerException) - : base(message, innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - public ForbiddenException(string format, Exception innerException, params object[] args) - : base(string.Format(format, args), innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - protected ForbiddenException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Exceptions/InternalServerErrorException.cs b/redmine-net20-api/Exceptions/InternalServerErrorException.cs deleted file mode 100644 index 2686df36..00000000 --- a/redmine-net20-api/Exceptions/InternalServerErrorException.cs +++ /dev/null @@ -1,84 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Runtime.Serialization; - -namespace Redmine.Net.Api.Exceptions -{ - /// - /// - /// - public class InternalServerErrorException : RedmineException - { - /// - /// Initializes a new instance of the class. - /// - public InternalServerErrorException() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - public InternalServerErrorException(string message) - : base(message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public InternalServerErrorException(string format, params object[] args) - : base(string.Format(format, args)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public InternalServerErrorException(string message, Exception innerException) - : base(message, innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - public InternalServerErrorException(string format, Exception innerException, params object[] args) - : base(string.Format(format, args), innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - protected InternalServerErrorException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Exceptions/NameResolutionFailureException.cs b/redmine-net20-api/Exceptions/NameResolutionFailureException.cs deleted file mode 100755 index 7d7fcfa9..00000000 --- a/redmine-net20-api/Exceptions/NameResolutionFailureException.cs +++ /dev/null @@ -1,84 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Runtime.Serialization; - -namespace Redmine.Net.Api.Exceptions -{ - /// - /// - /// - public class NameResolutionFailureException : RedmineException - { - /// - /// Initializes a new instance of the class. - /// - public NameResolutionFailureException() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - public NameResolutionFailureException(string message) - : base(message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public NameResolutionFailureException(string format, params object[] args) - : base(string.Format(format, args)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public NameResolutionFailureException(string message, Exception innerException) - : base(message, innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - public NameResolutionFailureException(string format, Exception innerException, params object[] args) - : base(string.Format(format, args), innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - protected NameResolutionFailureException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Exceptions/NotAcceptableException.cs b/redmine-net20-api/Exceptions/NotAcceptableException.cs deleted file mode 100755 index 0405c6c1..00000000 --- a/redmine-net20-api/Exceptions/NotAcceptableException.cs +++ /dev/null @@ -1,84 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Runtime.Serialization; - -namespace Redmine.Net.Api.Exceptions -{ - /// - /// - /// - public class NotAcceptableException : RedmineException - { - /// - /// Initializes a new instance of the class. - /// - public NotAcceptableException() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - public NotAcceptableException(string message) - : base(message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public NotAcceptableException(string format, params object[] args) - : base(string.Format(format, args)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public NotAcceptableException(string message, Exception innerException) - : base(message, innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - public NotAcceptableException(string format, Exception innerException, params object[] args) - : base(string.Format(format, args), innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - protected NotAcceptableException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Exceptions/NotFoundException.cs b/redmine-net20-api/Exceptions/NotFoundException.cs deleted file mode 100755 index 51fba2d4..00000000 --- a/redmine-net20-api/Exceptions/NotFoundException.cs +++ /dev/null @@ -1,85 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Runtime.Serialization; - -namespace Redmine.Net.Api.Exceptions -{ - /// - /// Thrown in case the objects requested for could not be found. - /// - /// - public class NotFoundException : RedmineException - { - /// - /// Initializes a new instance of the class. - /// - public NotFoundException() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - public NotFoundException(string message) - : base(message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public NotFoundException(string format, params object[] args) - : base(string.Format(format, args)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public NotFoundException(string message, Exception innerException) - : base(message, innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - public NotFoundException(string format, Exception innerException, params object[] args) - : base(string.Format(format, args), innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - protected NotFoundException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Exceptions/RedmineException.cs b/redmine-net20-api/Exceptions/RedmineException.cs deleted file mode 100755 index ea9e0289..00000000 --- a/redmine-net20-api/Exceptions/RedmineException.cs +++ /dev/null @@ -1,85 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Runtime.Serialization; - -namespace Redmine.Net.Api.Exceptions -{ - /// - /// Thrown in case something went wrong in Redmine - /// - /// - public class RedmineException : Exception - { - /// - /// Initializes a new instance of the class. - /// - public RedmineException() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public RedmineException(string message) - : base(message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The format. - /// The arguments. - public RedmineException(string format, params object[] args) - : base(string.Format(format, args)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if no inner exception is specified. - public RedmineException(string message, Exception innerException) - : base(message, innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The format. - /// The inner exception. - /// The arguments. - public RedmineException(string format, Exception innerException, params object[] args) - : base(string.Format(format, args), innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected RedmineException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Exceptions/RedmineTimeoutException.cs b/redmine-net20-api/Exceptions/RedmineTimeoutException.cs deleted file mode 100755 index 4dd6938b..00000000 --- a/redmine-net20-api/Exceptions/RedmineTimeoutException.cs +++ /dev/null @@ -1,93 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Runtime.Serialization; - -namespace Redmine.Net.Api.Exceptions -{ - /// - /// - /// - public class RedmineTimeoutException : RedmineException - { - /// - /// Initializes a new instance of the class. - /// - public RedmineTimeoutException() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public RedmineTimeoutException(string message) - : base(message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The format. - /// The arguments. - public RedmineTimeoutException(string format, params object[] args) - : base(string.Format(format, args)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// - /// The exception that is the cause of the current exception, or a null reference (Nothing in - /// Visual Basic) if no inner exception is specified. - /// - public RedmineTimeoutException(string message, Exception innerException) - : base(message, innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The format. - /// The inner exception. - /// The arguments. - public RedmineTimeoutException(string format, Exception innerException, params object[] args) - : base(string.Format(format, args), innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// The that holds the serialized object - /// data about the exception being thrown. - /// - /// - /// The that contains contextual - /// information about the source or destination. - /// - protected RedmineTimeoutException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Exceptions/UnauthorizedException.cs b/redmine-net20-api/Exceptions/UnauthorizedException.cs deleted file mode 100755 index d94d30dd..00000000 --- a/redmine-net20-api/Exceptions/UnauthorizedException.cs +++ /dev/null @@ -1,94 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Runtime.Serialization; - -namespace Redmine.Net.Api.Exceptions -{ - /// - /// Thrown in case something went wrong while trying to login. - /// - /// - public class UnauthorizedException : RedmineException - { - /// - /// Initializes a new instance of the class. - /// - public UnauthorizedException() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public UnauthorizedException(string message) - : base(message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The format. - /// The arguments. - public UnauthorizedException(string format, params object[] args) - : base(string.Format(format, args)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// - /// The exception that is the cause of the current exception, or a null reference (Nothing in - /// Visual Basic) if no inner exception is specified. - /// - public UnauthorizedException(string message, Exception innerException) - : base(message, innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The format. - /// The inner exception. - /// The arguments. - public UnauthorizedException(string format, Exception innerException, params object[] args) - : base(string.Format(format, args), innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// The that holds the serialized object - /// data about the exception being thrown. - /// - /// - /// The that contains contextual - /// information about the source or destination. - /// - protected UnauthorizedException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Extensions/CollectionExtensions.cs b/redmine-net20-api/Extensions/CollectionExtensions.cs deleted file mode 100755 index fe17f3d9..00000000 --- a/redmine-net20-api/Extensions/CollectionExtensions.cs +++ /dev/null @@ -1,61 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2016 Adrian Popescu - - 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. -*/ - -using System; -using System.Collections.Generic; - -namespace Redmine.Net.Api.Extensions -{ - /// - /// - /// - public static class CollectionExtensions - { - /// - /// Clones the specified list to clone. - /// - /// - /// The list to clone. - /// - public static IList Clone(this IList listToClone) where T : ICloneable - { - if (listToClone == null) return null; - IList clonedList = new List(); - foreach (T item in listToClone) - clonedList.Add((T)item.Clone()); - return clonedList; - } - - /// - /// Equalses the specified list to compare. - /// - /// - /// The list. - /// The list to compare. - /// - public static bool Equals(this IList list, IList listToCompare) where T : class - { - if (listToCompare == null) return false; - - if (list.Count != listToCompare.Count) return false; - - var index = 0; - while (index < list.Count && (list[index] as T).Equals(listToCompare[index] as T)) index++; - - return index == list.Count; - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Extensions/LoggerExtensions.cs b/redmine-net20-api/Extensions/LoggerExtensions.cs deleted file mode 100755 index 9e836a19..00000000 --- a/redmine-net20-api/Extensions/LoggerExtensions.cs +++ /dev/null @@ -1,52 +0,0 @@ -/* - Copyright 2011 - 2016 Adrian Popescu - - 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. -*/ - -using Redmine.Net.Api.Logging; - -namespace Redmine.Net.Api.Extensions -{ - /// - /// - public static class LoggerExtensions - { - /// - /// Uses the console log. - /// - /// The redmine manager. - public static void UseConsoleLog(this RedmineManager redmineManager) - { - Logger.UseLogger(new ConsoleLogger()); - } - - /// - /// Uses the color console log. - /// - /// The redmine manager. - public static void UseColorConsoleLog(this RedmineManager redmineManager) - { - Logger.UseLogger(new ColorConsoleLogger()); - } - - /// - /// Uses the trace log. - /// - /// The redmine manager. - public static void UseTraceLog(this RedmineManager redmineManager) - { - Logger.UseLogger(new TraceLogger()); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Extensions/NameValueCollectionExtensions.cs b/redmine-net20-api/Extensions/NameValueCollectionExtensions.cs deleted file mode 100644 index 20c2a989..00000000 --- a/redmine-net20-api/Extensions/NameValueCollectionExtensions.cs +++ /dev/null @@ -1,39 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu - - 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. -*/ - -using System.Collections.Specialized; - -namespace Redmine.Net.Api.Extensions -{ - /// - /// - /// - public static class NameValueCollectionExtensions - { - /// - /// Gets the parameter value. - /// - /// The parameters. - /// Name of the parameter. - /// - public static string GetParameterValue(this NameValueCollection parameters, string parameterName) - { - if (parameters == null) return null; - var value = parameters.Get(parameterName); - return string.IsNullOrEmpty(value) ? null : value; - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Extensions/WebExtensions.cs b/redmine-net20-api/Extensions/WebExtensions.cs deleted file mode 100644 index 18957c24..00000000 --- a/redmine-net20-api/Extensions/WebExtensions.cs +++ /dev/null @@ -1,130 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Logging; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.Extensions -{ - /// - /// - public static class WebExtensions - { - /// - /// Handles the web exception. - /// - /// The exception. - /// The method. - /// The MIME format. - /// Timeout! - /// Bad domain name! - /// - /// - /// - /// - /// The page that you are trying to update is staled! - /// - /// - /// - public static void HandleWebException(this WebException exception, string method, MimeFormat mimeFormat) - { - if (exception == null) return; - - switch (exception.Status) - { - case WebExceptionStatus.Timeout: - throw new RedmineTimeoutException("Timeout!", exception); - case WebExceptionStatus.NameResolutionFailure: - throw new NameResolutionFailureException("Bad domain name!", exception); - case WebExceptionStatus.ProtocolError: - { - var response = (HttpWebResponse) exception.Response; - switch ((int) response.StatusCode) - { - case (int) HttpStatusCode.NotFound: - throw new NotFoundException(response.StatusDescription, exception); - - case (int) HttpStatusCode.InternalServerError: - throw new InternalServerErrorException(response.StatusDescription, exception); - - case (int) HttpStatusCode.Unauthorized: - throw new UnauthorizedException(response.StatusDescription, exception); - - case (int) HttpStatusCode.Forbidden: - throw new ForbiddenException(response.StatusDescription, exception); - - case (int) HttpStatusCode.Conflict: - throw new ConflictException("The page that you are trying to update is staled!", exception); - - case 422: - var errors = GetRedmineExceptions(exception.Response, mimeFormat); - var message = string.Empty; - if (errors != null) - { - foreach (var error in errors) - message = message + error.Info + "\n"; - } - throw new RedmineException( - method + " has invalid or missing attribute parameters: " + message, exception); - - case (int) HttpStatusCode.NotAcceptable: - throw new NotAcceptableException(response.StatusDescription, exception); - } - } - break; - - default: - throw new RedmineException(exception.Message, exception); - } - } - - /// - /// Gets the redmine exceptions. - /// - /// The web response. - /// The MIME format. - /// - private static List GetRedmineExceptions(this WebResponse webResponse, MimeFormat mimeFormat) - { - using (var dataStream = webResponse.GetResponseStream()) - { - if (dataStream == null) return null; - using (var reader = new StreamReader(dataStream)) - { - var responseFromServer = reader.ReadToEnd(); - - if (string.IsNullOrEmpty(responseFromServer.Trim())) return null; - try - { - var result = RedmineSerializer.DeserializeList(responseFromServer, mimeFormat); - return result.Objects; - } - catch (Exception ex) - { - Logger.Current.Error(ex.Message); - } - } - return null; - } - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Extensions/XmlReaderExtensions.cs b/redmine-net20-api/Extensions/XmlReaderExtensions.cs deleted file mode 100755 index 1a5626ea..00000000 --- a/redmine-net20-api/Extensions/XmlReaderExtensions.cs +++ /dev/null @@ -1,178 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Xml; -using System.Xml.Serialization; - -namespace Redmine.Net.Api.Extensions -{ - /// - /// - /// - public static partial class XmlExtensions - { - /// - /// Reads the attribute as int. - /// - /// The reader. - /// Name of the attribute. - /// - public static int ReadAttributeAsInt(this XmlReader reader, string attributeName) - { - var attribute = reader.GetAttribute(attributeName); - int result; - if (string.IsNullOrEmpty(attribute) || !int.TryParse(attribute, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out result)) return default(int); - - return result; - } - - /// - /// Reads the attribute as nullable int. - /// - /// The reader. - /// Name of the attribute. - /// - public static int? ReadAttributeAsNullableInt(this XmlReader reader, string attributeName) - { - var attribute = reader.GetAttribute(attributeName); - int result; - if (string.IsNullOrEmpty(attribute) || !int.TryParse(attribute, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out result)) return default(int?); - - return result; - } - - /// - /// Reads the attribute as boolean. - /// - /// The reader. - /// Name of the attribute. - /// - public static bool ReadAttributeAsBoolean(this XmlReader reader, string attributeName) - { - var attribute = reader.GetAttribute(attributeName); - bool result; - if (string.IsNullOrEmpty(attribute) || !bool.TryParse(attribute, out result)) return false; - - return result; - } - - /// - /// Reads the element content as nullable date time. - /// - /// The reader. - /// - public static DateTime? ReadElementContentAsNullableDateTime(this XmlReader reader) - { - var str = reader.ReadElementContentAsString(); - DateTime result; - if (string.IsNullOrEmpty(str) || !DateTime.TryParse(str, out result)) return null; - - return result; - } - - /// - /// Reads the element content as nullable float. - /// - /// The reader. - /// - public static float? ReadElementContentAsNullableFloat(this XmlReader reader) - { - var str = reader.ReadElementContentAsString(); - float result; - if (string.IsNullOrEmpty(str) || !float.TryParse(str, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out result)) return null; - - return result; - } - - /// - /// Reads the element content as nullable int. - /// - /// The reader. - /// - public static int? ReadElementContentAsNullableInt(this XmlReader reader) - { - var str = reader.ReadElementContentAsString(); - int result; - if (string.IsNullOrEmpty(str) || !int.TryParse(str, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out result)) return null; - - return result; - } - - /// - /// Reads the element content as nullable decimal. - /// - /// The reader. - /// - public static decimal? ReadElementContentAsNullableDecimal(this XmlReader reader) - { - var str = reader.ReadElementContentAsString(); - decimal result; - if (string.IsNullOrEmpty(str) || !decimal.TryParse(str, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out result)) return null; - - return result; - } - - /// - /// Reads the element content as collection. - /// - /// - /// The reader. - /// - public static List ReadElementContentAsCollection(this XmlReader reader) where T : class - { - var result = new List(); - var serializer = new XmlSerializer(typeof(T)); - var xml = reader.ReadOuterXml(); - using (var sr = new StringReader(xml)) - { - using (var xmlTextReader = new XmlTextReader(sr)) - { - xmlTextReader.ReadStartElement(); - while (!xmlTextReader.EOF) - { - if (xmlTextReader.NodeType == XmlNodeType.EndElement) - { - xmlTextReader.ReadEndElement(); - continue; - } - - T obj; - - if (xmlTextReader.IsEmptyElement && xmlTextReader.HasAttributes) - { - obj = serializer.Deserialize(xmlTextReader) as T; - } - else - { - var subTree = xmlTextReader.ReadSubtree(); - obj = serializer.Deserialize(subTree) as T; - } - if (obj != null) - result.Add(obj); - - if (!xmlTextReader.IsEmptyElement) - xmlTextReader.Read(); - } - } - } - return result; - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Extensions/XmlWriterExtensions.cs b/redmine-net20-api/Extensions/XmlWriterExtensions.cs deleted file mode 100755 index 6ba64341..00000000 --- a/redmine-net20-api/Extensions/XmlWriterExtensions.cs +++ /dev/null @@ -1,200 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.Xml; -using System.Xml.Serialization; -using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.Extensions -{ - /// - /// - /// - public static partial class XmlExtensions - { - /// - /// Writes the id if not null. - /// - /// The writer. - /// The ident. - /// The tag. - public static void WriteIdIfNotNull(this XmlWriter writer, IdentifiableName ident, string tag) - { - if (ident != null) writer.WriteElementString(tag, ident.Id.ToString(CultureInfo.InvariantCulture)); - } - - /// - /// Writes the array. - /// - /// The writer. - /// The collection. - /// Name of the element. - public static void WriteArray(this XmlWriter writer, IEnumerable collection, string elementName) - { - if (collection == null) return; - writer.WriteStartElement(elementName); - writer.WriteAttributeString("type", "array"); - - foreach (var item in collection) - { - new XmlSerializer(item.GetType()).Serialize(writer, item); - } - - writer.WriteEndElement(); - } - - /// - /// Writes the array ids. - /// - /// The writer. - /// The collection. - /// Name of the element. - /// The type. - /// The f. - public static void WriteArrayIds(this XmlWriter writer, IEnumerable collection, string elementName, Type type, Func f) - { - if (collection == null) return; - writer.WriteStartElement(elementName); - writer.WriteAttributeString("type", "array"); - - foreach (var item in collection) - { - new XmlSerializer(type).Serialize(writer, f.Invoke(item)); - } - - writer.WriteEndElement(); - } - - /// - /// Writes the array. - /// - /// The writer. - /// The collection. - /// Name of the element. - /// The type. - /// The root. - /// The default namespace. - public static void WriteArray(this XmlWriter writer, IEnumerable collection, string elementName, Type type, string root, string defaultNamespace = null) - { - if (collection == null) return; - writer.WriteStartElement(elementName); - writer.WriteAttributeString("type", "array"); - - foreach (var item in collection) - { - new XmlSerializer(type, new XmlAttributeOverrides(), new Type[] { }, new XmlRootAttribute(root), - defaultNamespace).Serialize(writer, item); - } - - writer.WriteEndElement(); - } - - /// - /// Writes the array string element. - /// - /// The writer. - /// The collection. - /// Name of the element. - /// The func to invoke. - public static void WriteArrayStringElement(this XmlWriter writer, IEnumerable collection, string elementName, Func f) - { - if (collection == null) return; - writer.WriteStartElement(elementName); - writer.WriteAttributeString("type", "array"); - - foreach (var item in collection) - { - writer.WriteElementString(elementName, f.Invoke(item)); - } - writer.WriteEndElement(); - } - - /// - /// Writes the list elements. - /// - /// The XML writer. - /// The collection. - /// Name of the element. - public static void WriteListElements(this XmlWriter xmlWriter, IEnumerable collection, string elementName) - { - if (collection == null) return; - - foreach (var item in collection) - { - xmlWriter.WriteElementString(elementName, item.Value); - } - } - - /// - /// Writes the identifier or empty. - /// - /// The writer. - /// The ident. - /// The tag. - public static void WriteIdOrEmpty(this XmlWriter writer, IdentifiableName ident, string tag) - { - writer.WriteElementString(tag, ident != null ? ident.Id.ToString(CultureInfo.InvariantCulture) : string.Empty); - } - - /// - /// Writes if not default or null. - /// - /// - /// The writer. - /// The value. - /// The tag. - public static void WriteIfNotDefaultOrNull(this XmlWriter writer, T? val, string tag) where T : struct - { - if (!val.HasValue) return; - if (!EqualityComparer.Default.Equals(val.Value, default(T))) - writer.WriteElementString(tag, string.Format(NumberFormatInfo.InvariantInfo, "{0}", val.Value)); - } - - /// - /// Writes string empty if T has default value or null. - /// - /// - /// The writer. - /// The value. - /// The tag. - public static void WriteValueOrEmpty(this XmlWriter writer, T? val, string tag) where T : struct - { - if (!val.HasValue || EqualityComparer.Default.Equals(val.Value, default(T))) - writer.WriteElementString(tag, string.Empty); - else - writer.WriteElementString(tag, string.Format(NumberFormatInfo.InvariantInfo, "{0}", val.Value)); - } - - /// - /// Writes the date or empty. - /// - /// The writer. - /// The value. - /// The tag. - public static void WriteDateOrEmpty(this XmlWriter writer, DateTime? val, string tag) - { - if (!val.HasValue || val.Value.Equals(default(DateTime))) - writer.WriteElementString(tag, string.Empty); - else - writer.WriteElementString(tag, string.Format(NumberFormatInfo.InvariantInfo, "{0}", val.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture))); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/HttpVerbs.cs b/redmine-net20-api/HttpVerbs.cs deleted file mode 100755 index 88dafe62..00000000 --- a/redmine-net20-api/HttpVerbs.cs +++ /dev/null @@ -1,42 +0,0 @@ -ο»Ώ/* -Copyright 2011 - 2017 Adrian Popescu. - -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. -*/ - -namespace Redmine.Net.Api -{ - - /// - /// - /// - public static class HttpVerbs - { - /// - /// Represents an HTTP PUT protocol method that is used to replace an entity identified by a URI. - /// - public const string PUT = "PUT"; - /// - /// Represents an HTTP POST protocol method that is used to post a new entity as an addition to a URI. - /// - public const string POST = "POST"; - /// - /// Represents an HTTP PATCH protocol method that is used to patch an existing entity identified by a URI. - /// - public const string PATCH = "PATCH"; - /// - /// Represents an HTTP DELETE protocol method that is used to delete an existing entity identified by a URI. - /// - public const string DELETE = "DELETE"; - } -} \ No newline at end of file diff --git a/redmine-net20-api/Internals/DataHelper.cs b/redmine-net20-api/Internals/DataHelper.cs deleted file mode 100755 index cc61e88d..00000000 --- a/redmine-net20-api/Internals/DataHelper.cs +++ /dev/null @@ -1,37 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -namespace Redmine.Net.Api.Internals -{ - /// - /// - /// - internal static class DataHelper - { - /// - /// Users the data. - /// - /// The user identifier. - /// The MIME format. - /// - public static string UserData(int userId, MimeFormat mimeFormat) - { - return mimeFormat == MimeFormat.Xml - ? "" + userId + "" - : "{\"user_id\":\"" + userId + "\"}"; - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Internals/RedmineSerializer.cs b/redmine-net20-api/Internals/RedmineSerializer.cs deleted file mode 100644 index 1fb84a67..00000000 --- a/redmine-net20-api/Internals/RedmineSerializer.cs +++ /dev/null @@ -1,170 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.IO; -using System.Xml; -using System.Xml.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Exceptions; - -namespace Redmine.Net.Api.Internals -{ - /// - /// - /// - internal static class RedmineSerializer - { - /// - /// Serializes the specified System.Object and writes the XML document to a string. - /// - /// The type of objects to serialize. - /// The object to serialize. - /// - /// The System.String that contains the XML document. - /// - /// - /// - // ReSharper disable once InconsistentNaming - private static string ToXML(T obj) where T : class - { - var xws = new XmlWriterSettings { OmitXmlDeclaration = true }; - using (var stringWriter = new StringWriter()) - { - using (var xmlWriter = XmlWriter.Create(stringWriter, xws)) - { - var sr = new XmlSerializer(typeof(T)); - sr.Serialize(xmlWriter, obj); - return stringWriter.ToString(); - } - } - } - - /// - /// Deserializes the XML document contained by the specific System.String. - /// - /// The type of objects to deserialize. - /// The System.String that contains the XML document to deserialize. - /// - /// The T object being deserialized. - /// - /// An error occurred during deserialization. The original exception is available - /// using the System.Exception.InnerException property. - // ReSharper disable once InconsistentNaming - private static T FromXML(string xml) where T : class - { - using (var text = new StringReader(xml)) - { - var sr = new XmlSerializer(typeof(T)); - return sr.Deserialize(text) as T; - } - } - - /// - /// Deserializes the XML document contained by the specific System.String. - /// - /// The System.String that contains the XML document to deserialize. - /// The type of objects to deserialize. - /// - /// The System.Object being deserialized. - /// - /// An error occurred during deserialization. The original exception is available - /// using the System.Exception.InnerException property. - // ReSharper disable once InconsistentNaming - private static object FromXML(string xml, Type type) - { - using (var text = new StringReader(xml)) - { - var sr = new XmlSerializer(type); - return sr.Deserialize(text); - } - } - - /// - /// Serializes the specified object. - /// - /// - /// The object. - /// The MIME format. - /// - public static string Serialize(T obj, MimeFormat mimeFormat) where T : class, new() - { - return ToXML(obj); - } - - /// - /// Deserializes the specified response. - /// - /// - /// The response. - /// The MIME format. - /// - /// could not deserialize: + response - public static T Deserialize(string response, MimeFormat mimeFormat) where T : class, new() - { - if (string.IsNullOrEmpty(response)) throw new RedmineException("could not deserialize: " + response); - - return FromXML(response); - } - - /// - /// Deserializes the list. - /// - /// - /// The response. - /// The MIME format. - /// - /// web response is null! - public static PaginatedObjects DeserializeList(string response, MimeFormat mimeFormat) where T : class, new() - { - if (string.IsNullOrEmpty(response)) throw new RedmineException("web response is null!"); - - return XmlDeserializeList(response); - } - - /// - /// XMLs the deserialize list. - /// - /// - /// The response. - /// - /// could not deserialize: + response - private static PaginatedObjects XmlDeserializeList(string response) where T : class, new() - { - if (string.IsNullOrEmpty(response)) throw new RedmineException("could not deserialize: " + response); - - using (var stringReader = new StringReader(response)) - { - using (var xmlReader = new XmlTextReader(stringReader)) - { - xmlReader.WhitespaceHandling = WhitespaceHandling.None; - xmlReader.Read(); - xmlReader.Read(); - - var totalItems = xmlReader.ReadAttributeAsInt(RedmineKeys.TOTAL_COUNT); - - var result = xmlReader.ReadElementContentAsCollection(); - return new PaginatedObjects() - { - TotalCount = totalItems, - Objects = result - }; - } - } - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Internals/UrlHelper.cs b/redmine-net20-api/Internals/UrlHelper.cs deleted file mode 100755 index d40e30fa..00000000 --- a/redmine-net20-api/Internals/UrlHelper.cs +++ /dev/null @@ -1,371 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.Internals -{ - /// - /// - internal static class UrlHelper - { - /// - /// - const string REQUEST_FORMAT = "{0}/{1}/{2}.{3}"; - - /// - /// - const string FORMAT = "{0}/{1}.{2}"; - - /// - /// - const string WIKI_INDEX_FORMAT = "{0}/projects/{1}/wiki/index.{2}"; - - /// - /// - const string WIKI_PAGE_FORMAT = "{0}/projects/{1}/wiki/{2}.{3}"; - - /// - /// - const string WIKI_VERSION_FORMAT = "{0}/projects/{1}/wiki/{2}/{3}.{4}"; - - /// - /// - const string ENTITY_WITH_PARENT_FORMAT = "{0}/{1}/{2}/{3}.{4}"; - - /// - /// - const string ATTACHMENT_UPDATE_FORMAT = "{0}/attachments/issues/{1}.{2}"; - - /// - /// - /// - const string FILE_URL_FORMAT = "{0}/projects/{1}/files.{2}"; - - - /// - /// - const string CURRENT_USER_URI = "current"; - /// - /// Gets the upload URL. - /// - /// - /// The redmine manager. - /// The identifier. - /// The object. - /// The project identifier. - /// - /// - public static string GetUploadUrl(RedmineManager redmineManager, string id, T obj, string projectId = null) - where T : class, new() - { - var type = typeof(T); - - if (!RedmineManager.Sufixes.ContainsKey(type)) throw new KeyNotFoundException(type.Name); - - return string.Format(REQUEST_FORMAT, redmineManager.Host, RedmineManager.Sufixes[type], id, - redmineManager.MimeFormat.ToString().ToLower()); - } - - /// - /// Gets the create URL. - /// - /// - /// The redmine manager. - /// The owner identifier. - /// - /// - /// - /// The owner id(project id) is mandatory! - /// or - /// The owner id(issue id) is mandatory! - /// - public static string GetCreateUrl(RedmineManager redmineManager, string ownerId) where T : class, new() - { - var type = typeof(T); - - if (!RedmineManager.Sufixes.ContainsKey(type)) throw new KeyNotFoundException(type.Name); - - if (type == typeof(Version) || type == typeof(IssueCategory) || type == typeof(ProjectMembership)) - { - if (string.IsNullOrEmpty(ownerId)) throw new RedmineException("The owner id(project id) is mandatory!"); - return string.Format(ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.PROJECTS, - ownerId, RedmineManager.Sufixes[type], redmineManager.MimeFormat.ToString().ToLower()); - } - if (type == typeof(IssueRelation)) - { - if (string.IsNullOrEmpty(ownerId)) throw new RedmineException("The owner id(issue id) is mandatory!"); - return string.Format(ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.ISSUES, - ownerId, RedmineManager.Sufixes[type], redmineManager.MimeFormat.ToString().ToLower()); - } - - if (type == typeof(File)) - { - if (string.IsNullOrEmpty(ownerId)) - { - throw new RedmineException("The owner id(project id) is mandatory!"); - } - return string.Format(FILE_URL_FORMAT, redmineManager.Host, ownerId, redmineManager.MimeFormat.ToString().ToLower()); - } - - return string.Format(FORMAT, redmineManager.Host, RedmineManager.Sufixes[type], - redmineManager.MimeFormat.ToString().ToLower()); - } - - /// - /// Gets the delete URL. - /// - /// - /// The redmine manager. - /// The identifier. - /// - /// - /// - public static string GetDeleteUrl(RedmineManager redmineManager, string id) where T : class, new() - { - var type = typeof(T); - - if (!RedmineManager.Sufixes.ContainsKey(type)) throw new KeyNotFoundException(type.Name); - - return string.Format(REQUEST_FORMAT, redmineManager.Host, RedmineManager.Sufixes[type], id, - redmineManager.MimeFormat.ToString().ToLower()); - } - - /// - /// Gets the get URL. - /// - /// - /// The redmine manager. - /// The identifier. - /// - /// - public static string GetGetUrl(RedmineManager redmineManager, string id) where T : class, new() - { - var type = typeof(T); - - if (!RedmineManager.Sufixes.ContainsKey(type)) throw new KeyNotFoundException(type.Name); - - return string.Format(REQUEST_FORMAT, redmineManager.Host, RedmineManager.Sufixes[type], id, - redmineManager.MimeFormat.ToString().ToLower()); - } - - /// - /// Gets the list URL. - /// - /// - /// The redmine manager. - /// The parameters. - /// - /// - /// - /// The project id is mandatory! \nCheck if you have included the parameter project_id to parameters. - /// or - /// The issue id is mandatory! \nCheck if you have included the parameter issue_id to parameters - /// - public static string GetListUrl(RedmineManager redmineManager, NameValueCollection parameters) - where T : class, new() - { - var type = typeof(T); - - if (!RedmineManager.Sufixes.ContainsKey(type)) throw new KeyNotFoundException(type.Name); - - if (type == typeof(Version) || type == typeof(IssueCategory) || type == typeof(ProjectMembership)) - { - var projectId = parameters.GetParameterValue(RedmineKeys.PROJECT_ID); - if (string.IsNullOrEmpty(projectId)) - throw new RedmineException("The project id is mandatory! \nCheck if you have included the parameter project_id to parameters."); - - return string.Format(ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.PROJECTS, - projectId, RedmineManager.Sufixes[type], redmineManager.MimeFormat.ToString().ToLower()); - } - if (type == typeof(IssueRelation)) - { - var issueId = parameters.GetParameterValue(RedmineKeys.ISSUE_ID); - if (string.IsNullOrEmpty(issueId)) - throw new RedmineException("The issue id is mandatory! \nCheck if you have included the parameter issue_id to parameters"); - - return string.Format(ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.ISSUES, - issueId, RedmineManager.Sufixes[type], redmineManager.MimeFormat.ToString().ToLower()); - } - - if (type == typeof(File)) - { - var projectId = parameters.GetParameterValue(RedmineKeys.PROJECT_ID); - if (string.IsNullOrEmpty(projectId)) - { - throw new RedmineException("The project id is mandatory! \nCheck if you have included the parameter project_id to parameters."); - } - return string.Format(FILE_URL_FORMAT, redmineManager.Host, projectId, redmineManager.MimeFormat.ToString().ToLower()); - } - - return string.Format(FORMAT, redmineManager.Host, RedmineManager.Sufixes[type], - redmineManager.MimeFormat.ToString().ToLower()); - } - - /// - /// Gets the wikis URL. - /// - /// The redmine manager. - /// The project identifier. - /// - public static string GetWikisUrl(RedmineManager redmineManager, string projectId) - { - return string.Format(WIKI_INDEX_FORMAT, redmineManager.Host, projectId, - redmineManager.MimeFormat.ToString().ToLower()); - } - - /// - /// Gets the wiki page URL. - /// - /// The redmine manager. - /// The project identifier. - /// The parameters. - /// Name of the page. - /// The version. - /// - public static string GetWikiPageUrl(RedmineManager redmineManager, string projectId, - NameValueCollection parameters, string pageName, uint version = 0) - { - var uri = version == 0 - ? string.Format(WIKI_PAGE_FORMAT, redmineManager.Host, projectId, pageName, - redmineManager.MimeFormat.ToString().ToLower()) - : string.Format(WIKI_VERSION_FORMAT, redmineManager.Host, projectId, pageName, version, - redmineManager.MimeFormat.ToString().ToLower()); - return uri; - } - - /// - /// Gets the add user to group URL. - /// - /// The redmine manager. - /// The group identifier. - /// - public static string GetAddUserToGroupUrl(RedmineManager redmineManager, int groupId) - { - return string.Format(REQUEST_FORMAT, redmineManager.Host, - RedmineManager.Sufixes[typeof(Group)], - groupId + "/users", redmineManager.MimeFormat.ToString().ToLower()); - } - - /// - /// Gets the remove user from group URL. - /// - /// The redmine manager. - /// The group identifier. - /// The user identifier. - /// - public static string GetRemoveUserFromGroupUrl(RedmineManager redmineManager, int groupId, int userId) - { - return string.Format(REQUEST_FORMAT, redmineManager.Host, - RedmineManager.Sufixes[typeof(Group)], - groupId + "/users/" + userId, redmineManager.MimeFormat.ToString().ToLower()); - } - - /// - /// Gets the upload file URL. - /// - /// The redmine manager. - /// - public static string GetUploadFileUrl(RedmineManager redmineManager) - { - return string.Format(FORMAT, redmineManager.Host, RedmineKeys.UPLOADS, - redmineManager.MimeFormat.ToString().ToLower()); - } - - /// - /// Gets the current user URL. - /// - /// The redmine manager. - /// - public static string GetCurrentUserUrl(RedmineManager redmineManager) - { - return string.Format(REQUEST_FORMAT, redmineManager.Host, - RedmineManager.Sufixes[typeof(User)], CURRENT_USER_URI, - redmineManager.MimeFormat.ToString().ToLower()); - } - - /// - /// Gets the wiki create or updater URL. - /// - /// The redmine manager. - /// The project identifier. - /// Name of the page. - /// - public static string GetWikiCreateOrUpdaterUrl(RedmineManager redmineManager, string projectId, string pageName) - { - return string.Format(WIKI_PAGE_FORMAT, redmineManager.Host, projectId, pageName, - redmineManager.MimeFormat.ToString().ToLower()); - } - - /// - /// Gets the delete wikir URL. - /// - /// The redmine manager. - /// The project identifier. - /// Name of the page. - /// - public static string GetDeleteWikirUrl(RedmineManager redmineManager, string projectId, string pageName) - { - return string.Format(WIKI_PAGE_FORMAT, redmineManager.Host, projectId, pageName, - redmineManager.MimeFormat.ToString().ToLower()); - } - - /// - /// Gets the add watcher URL. - /// - /// The redmine manager. - /// The issue identifier. - /// The user identifier. - /// - public static string GetAddWatcherUrl(RedmineManager redmineManager, int issueId, int userId) - { - return string.Format(REQUEST_FORMAT, redmineManager.Host, - RedmineManager.Sufixes[typeof(Issue)], issueId + "/watchers", - redmineManager.MimeFormat.ToString().ToLower()); - } - - /// - /// Gets the remove watcher URL. - /// - /// The redmine manager. - /// The issue identifier. - /// The user identifier. - /// - public static string GetRemoveWatcherUrl(RedmineManager redmineManager, int issueId, int userId) - { - return string.Format(REQUEST_FORMAT, redmineManager.Host, - RedmineManager.Sufixes[typeof(Issue)], issueId + "/watchers/" + userId, - redmineManager.MimeFormat.ToString().ToLower()); - } - - /// - /// Gets the attachment update URL. - /// - /// The redmine manager. - /// The issue identifier. - /// - public static string GetAttachmentUpdateUrl(RedmineManager redmineManager, int issueId) - { - return string.Format(ATTACHMENT_UPDATE_FORMAT, redmineManager.Host, issueId, - redmineManager.MimeFormat.ToString().ToLower()); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Internals/WebApiHelper.cs b/redmine-net20-api/Internals/WebApiHelper.cs deleted file mode 100644 index ff5f498f..00000000 --- a/redmine-net20-api/Internals/WebApiHelper.cs +++ /dev/null @@ -1,198 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System.Collections.Specialized; -using System.Net; -using System.Text; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.Internals -{ - /// - /// - /// - internal static class WebApiHelper - { - /// - /// Executes the upload. - /// - /// The redmine manager. - /// The address. - /// Type of the action. - /// The data. - /// Name of the method. - /// The parameters - public static void ExecuteUpload(RedmineManager redmineManager, string address, string actionType, string data, - string methodName, NameValueCollection parameters = null) - { - using (var wc = redmineManager.CreateWebClient(parameters)) - { - try - { - if (actionType == HttpVerbs.POST || actionType == HttpVerbs.DELETE || actionType == HttpVerbs.PUT || - actionType == HttpVerbs.PATCH) - { - wc.UploadString(address, actionType, data); - } - } - catch (WebException webException) - { - webException.HandleWebException(methodName, redmineManager.MimeFormat); - } - } - } - - /// - /// Executes the upload. - /// - /// - /// The redmine manager. - /// The address. - /// Type of the action. - /// The data. - /// Name of the method. - /// - public static T ExecuteUpload(RedmineManager redmineManager, string address, string actionType, string data, - string methodName) - where T : class, new() - { - using (var wc = redmineManager.CreateWebClient(null)) - { - try - { - if (actionType == HttpVerbs.POST || actionType == HttpVerbs.DELETE || actionType == HttpVerbs.PUT || - actionType == HttpVerbs.PATCH) - { - var response = wc.UploadString(address, actionType, data); - return RedmineSerializer.Deserialize(response, redmineManager.MimeFormat); - } - } - catch (WebException webException) - { - webException.HandleWebException(methodName, redmineManager.MimeFormat); - } - return default(T); - } - } - - /// - /// Executes the download. - /// - /// - /// The redmine manager. - /// The address. - /// Name of the method. - /// The parameters. - /// - public static T ExecuteDownload(RedmineManager redmineManager, string address, string methodName, - NameValueCollection parameters = null) - where T : class, new() - { - using (var wc = redmineManager.CreateWebClient(parameters)) - { - try - { - var response = wc.DownloadString(address); - if (!string.IsNullOrEmpty(response)) - return RedmineSerializer.Deserialize(response, redmineManager.MimeFormat); - } - catch (WebException webException) - { - webException.HandleWebException(methodName, redmineManager.MimeFormat); - } - return default(T); - } - } - - /// - /// Executes the download list. - /// - /// - /// The redmine manager. - /// The address. - /// Name of the method. - /// The parameters. - /// - public static PaginatedObjects ExecuteDownloadList(RedmineManager redmineManager, string address, - string methodName, - NameValueCollection parameters = null) where T : class, new() - { - using (var wc = redmineManager.CreateWebClient(parameters)) - { - try - { - var response = wc.DownloadString(address); - return RedmineSerializer.DeserializeList(response, redmineManager.MimeFormat); - } - catch (WebException webException) - { - webException.HandleWebException(methodName, redmineManager.MimeFormat); - } - return null; - } - } - - /// - /// Executes the download file. - /// - /// The redmine manager. - /// The address. - /// Name of the method. - /// - public static byte[] ExecuteDownloadFile(RedmineManager redmineManager, string address, string methodName) - { - using (var wc = redmineManager.CreateWebClient(null, true)) - { - try - { - return wc.DownloadData(address); - } - catch (WebException webException) - { - webException.HandleWebException(methodName, redmineManager.MimeFormat); - } - return null; - } - } - - /// - /// Executes the upload file. - /// - /// The redmine manager. - /// The address. - /// The data. - /// Name of the method. - /// - public static Upload ExecuteUploadFile(RedmineManager redmineManager, string address, byte[] data, string methodName) - { - using (var wc = redmineManager.CreateWebClient(null, true)) - { - try - { - var response = wc.UploadData(address, data); - var responseString = Encoding.ASCII.GetString(response); - return RedmineSerializer.Deserialize(responseString, redmineManager.MimeFormat); - } - catch (WebException webException) - { - webException.HandleWebException(methodName, redmineManager.MimeFormat); - } - return null; - } - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Internals/XmlStreamingDeserializer.cs b/redmine-net20-api/Internals/XmlStreamingDeserializer.cs deleted file mode 100755 index 6e939e7b..00000000 --- a/redmine-net20-api/Internals/XmlStreamingDeserializer.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.IO; -using System.Xml; -using System.Xml.Serialization; - -namespace Redmine.Net.Api -{ - /// - /// - /// - /// - /// http://florianreischl.blogspot.ro/search/label/c%23 - public class XmlStreamingDeserializer - { - static XmlSerializerNamespaces ns; - XmlSerializer serializer; - XmlReader reader; - - static XmlStreamingDeserializer() - { - ns = new XmlSerializerNamespaces(); - ns.Add("", ""); - } - - private XmlStreamingDeserializer() - { - serializer = new XmlSerializer(typeof(T)); - } - - public XmlStreamingDeserializer(TextReader reader) - : this(XmlReader.Create(reader)) - { - } - - public XmlStreamingDeserializer(XmlReader reader) - : this() - { - this.reader = reader; - } - - public void Close() - { - reader.Close(); - } - - public T Deserialize() - { - while (reader.Read()) - { - if (reader.NodeType == XmlNodeType.Element && reader.Depth == 1 && reader.Name == typeof(T).Name) - { - XmlReader xmlReader = reader.ReadSubtree(); - return (T)serializer.Deserialize(xmlReader); - } - } - return default(T); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Internals/XmlStreamingSerializer.cs b/redmine-net20-api/Internals/XmlStreamingSerializer.cs deleted file mode 100755 index 56549eee..00000000 --- a/redmine-net20-api/Internals/XmlStreamingSerializer.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.IO; -using System.Xml; -using System.Xml.Serialization; - -namespace Redmine.Net.Api -{ - /// - /// - /// - /// - /// http://florianreischl.blogspot.ro/search/label/c%23 - public class XmlStreamingSerializer - { - static XmlSerializerNamespaces ns; - XmlSerializer serializer = new XmlSerializer(typeof(T)); - XmlWriter writer; - bool finished; - - static XmlStreamingSerializer() - { - ns = new XmlSerializerNamespaces(); - ns.Add("", ""); - } - - private XmlStreamingSerializer() - { - serializer = new XmlSerializer(typeof(T)); - } - - public XmlStreamingSerializer(TextWriter w) - : this(XmlWriter.Create(w)) - { - } - - public XmlStreamingSerializer(XmlWriter writer) - : this() - { - this.writer = writer; - writer.WriteStartDocument(); - writer.WriteStartElement("ArrayOf" + typeof(T).Name); - } - - public void Finish() - { - writer.WriteEndDocument(); - writer.Flush(); - finished = true; - } - - public void Close() - { - if (!finished) - Finish(); - writer.Close(); - } - - public void Serialize(T item) - { - serializer.Serialize(writer, item, ns); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Logging/ColorConsoleLogger.cs b/redmine-net20-api/Logging/ColorConsoleLogger.cs deleted file mode 100755 index a5b538fc..00000000 --- a/redmine-net20-api/Logging/ColorConsoleLogger.cs +++ /dev/null @@ -1,110 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; - -namespace Redmine.Net.Api.Logging -{ - /// - /// - /// - /// - public class ColorConsoleLogger : ILogger - { - private static readonly object locker = new object(); - private readonly ConsoleColor? defaultConsoleColor = null; - - /// - /// - /// - public void Log(LogEntry entry) - { - lock (locker) - { - var colors = GetLogLevelConsoleColors(entry.Severity); - switch (entry.Severity) - { - case LoggingEventType.Debug: - Console.WriteLine(entry.Message, colors.Background, colors.Foreground); - break; - case LoggingEventType.Information: - Console.WriteLine(entry.Message, colors.Background, colors.Foreground); - break; - case LoggingEventType.Warning: - Console.WriteLine(entry.Message, colors.Background, colors.Foreground); - break; - case LoggingEventType.Error: - Console.WriteLine(entry.Message, colors.Background, colors.Foreground); - break; - case LoggingEventType.Fatal: - Console.WriteLine(entry.Message, colors.Background, colors.Foreground); - break; - } - } - } - - /// - /// Gets the log level console colors. - /// - /// The log level. - /// - private ConsoleColors GetLogLevelConsoleColors(LoggingEventType logLevel) - { - // do not change user's background color except for Critical - switch (logLevel) - { - case LoggingEventType.Fatal: - return new ConsoleColors(ConsoleColor.White, ConsoleColor.Red); - case LoggingEventType.Error: - return new ConsoleColors(ConsoleColor.Red, defaultConsoleColor); - case LoggingEventType.Warning: - return new ConsoleColors(ConsoleColor.DarkYellow, defaultConsoleColor); - case LoggingEventType.Information: - return new ConsoleColors(ConsoleColor.DarkGreen, defaultConsoleColor); - default: - return new ConsoleColors(ConsoleColor.Gray, defaultConsoleColor); - } - } - - /// - /// - /// - private struct ConsoleColors - { - public ConsoleColors(ConsoleColor? foreground, ConsoleColor? background): this() - { - Foreground = foreground; - Background = background; - } - - /// - /// Gets or sets the foreground. - /// - /// - /// The foreground. - /// - public ConsoleColor? Foreground { get; private set; } - - /// - /// Gets or sets the background. - /// - /// - /// The background. - /// - public ConsoleColor? Background { get; private set; } - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Logging/ConsoleLogger.cs b/redmine-net20-api/Logging/ConsoleLogger.cs deleted file mode 100755 index 6e67dec0..00000000 --- a/redmine-net20-api/Logging/ConsoleLogger.cs +++ /dev/null @@ -1,56 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; - -namespace Redmine.Net.Api.Logging -{ - /// - /// - /// - /// - public class ConsoleLogger : ILogger - { - private static readonly object locker = new object(); - /// - /// - /// - public void Log(LogEntry entry) - { - lock (locker) - { - switch (entry.Severity) - { - case LoggingEventType.Debug: - Console.WriteLine(entry.Message); - break; - case LoggingEventType.Information: - Console.WriteLine(entry.Message); - break; - case LoggingEventType.Warning: - Console.WriteLine(entry.Message); - break; - case LoggingEventType.Error: - Console.WriteLine(entry.Message); - break; - case LoggingEventType.Fatal: - Console.WriteLine(entry.Message); - break; - } - } - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Logging/LogEntry.cs b/redmine-net20-api/Logging/LogEntry.cs deleted file mode 100755 index 9db551d7..00000000 --- a/redmine-net20-api/Logging/LogEntry.cs +++ /dev/null @@ -1,61 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; - -namespace Redmine.Net.Api.Logging -{ - /// - /// - /// - public class LogEntry - { - /// - /// Initializes a new instance of the class. - /// - /// The severity. - /// The message. - /// The exception. - public LogEntry(LoggingEventType severity, string message, Exception exception = null) - { - Severity = severity; - Message = message; - Exception = exception; - } - - /// - /// Gets the severity. - /// - /// - /// The severity. - /// - public LoggingEventType Severity { get; private set; } - /// - /// Gets the message. - /// - /// - /// The message. - /// - public string Message { get; private set; } - /// - /// Gets the exception. - /// - /// - /// The exception. - /// - public Exception Exception { get; private set; } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Logging/Logger.cs b/redmine-net20-api/Logging/Logger.cs deleted file mode 100755 index c5d5832c..00000000 --- a/redmine-net20-api/Logging/Logger.cs +++ /dev/null @@ -1,51 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -namespace Redmine.Net.Api.Logging -{ - /// - /// - /// - public static class Logger - { - private static readonly object locker = new object(); - private static ILogger logger; - - /// - /// Gets the current ILogger. - /// - /// - /// The current. - /// - public static ILogger Current - { - get { return logger ?? (logger = new ConsoleLogger()); } - private set { logger = value; } - } - - /// - /// Uses the logger. - /// - /// The logger. - public static void UseLogger(ILogger logger) - { - lock (locker) - { - Current = logger; - } - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Logging/LoggerExtensions.cs b/redmine-net20-api/Logging/LoggerExtensions.cs deleted file mode 100755 index eb721eac..00000000 --- a/redmine-net20-api/Logging/LoggerExtensions.cs +++ /dev/null @@ -1,225 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Globalization; - -namespace Redmine.Net.Api.Logging -{ - /// - /// - /// - public static class LoggerExtensions - { - /// - /// Debugs the specified message. - /// - /// The logger. - /// The message. - public static void Debug(this ILogger logger, string message) - { - logger.Log(new LogEntry(LoggingEventType.Debug, message)); - } - - /// - /// Debugs the specified message. - /// - /// The logger. - /// The message. - /// The exception. - public static void Debug(this ILogger logger, string message, Exception exception) - { - logger.Log(new LogEntry(LoggingEventType.Debug, message, exception)); - } - - /// - /// Debugs the specified format provider. - /// - /// The logger. - /// The format provider. - /// The format. - /// The arguments. - public static void Debug(this ILogger logger, IFormatProvider formatProvider, string format, params object[] args) - { - logger.Log(new LogEntry(LoggingEventType.Debug, string.Format(formatProvider, format, args))); - } - - /// - /// Debugs the specified format. - /// - /// The logger. - /// The format. - /// The arguments. - public static void Debug(this ILogger logger, string format, params object[] args) - { - logger.Log(new LogEntry(LoggingEventType.Debug, string.Format(CultureInfo.CurrentCulture, format, args))); - } - - /// - /// Informations the specified message. - /// - /// The logger. - /// The message. - public static void Information(this ILogger logger, string message) - { - logger.Log(new LogEntry(LoggingEventType.Information, message)); - } - - /// - /// Informations the specified format provider. - /// - /// The logger. - /// The format provider. - /// The format. - /// The arguments. - public static void Information(this ILogger logger, IFormatProvider formatProvider, string format, params object[] args) - { - logger.Log(new LogEntry(LoggingEventType.Information, string.Format(formatProvider, format, args))); - } - - /// - /// Informations the specified format. - /// - /// The logger. - /// The format. - /// The arguments. - public static void Information(this ILogger logger, string format, params object[] args) - { - logger.Log(new LogEntry(LoggingEventType.Information, string.Format(CultureInfo.CurrentCulture, format, args))); - } - - /// - /// Warnings the specified message. - /// - /// The logger. - /// The message. - public static void Warning(this ILogger logger, string message) - { - logger.Log(new LogEntry(LoggingEventType.Warning, message)); - } - - /// - /// Warnings the specified format provider. - /// - /// The logger. - /// The format provider. - /// The format. - /// The arguments. - public static void Warning(this ILogger logger, IFormatProvider formatProvider, string format, params object[] args) - { - logger.Log(new LogEntry(LoggingEventType.Warning, string.Format(formatProvider, format, args))); - } - - /// - /// Warnings the specified format. - /// - /// The logger. - /// The format. - /// The arguments. - public static void Warning(this ILogger logger, string format, params object[] args) - { - logger.Log(new LogEntry(LoggingEventType.Warning, string.Format(CultureInfo.CurrentCulture, format, args))); - } - - /// - /// Errors the specified exception. - /// - /// The logger. - /// The exception. - public static void Error(this ILogger logger, Exception exception) - { - logger.Log(new LogEntry(LoggingEventType.Error, exception.Message, exception)); - } - - /// - /// Errors the specified message. - /// - /// The logger. - /// The message. - /// The exception. - public static void Error(this ILogger logger, string message, Exception exception) - { - logger.Log(new LogEntry(LoggingEventType.Error, message, exception)); - } - - /// - /// Errors the specified format provider. - /// - /// The logger. - /// The format provider. - /// The format. - /// The arguments. - public static void Error(this ILogger logger, IFormatProvider formatProvider, string format, params object[] args) - { - logger.Log(new LogEntry(LoggingEventType.Error, string.Format(formatProvider, format, args))); - } - - /// - /// Errors the specified format. - /// - /// The logger. - /// The format. - /// The arguments. - public static void Error(this ILogger logger, string format, params object[] args) - { - logger.Log(new LogEntry(LoggingEventType.Error, string.Format(CultureInfo.CurrentCulture, format, args))); - } - - /// - /// Fatals the specified exception. - /// - /// The logger. - /// The exception. - public static void Fatal(this ILogger logger, Exception exception) - { - logger.Log(new LogEntry(LoggingEventType.Fatal, exception.Message, exception)); - } - - /// - /// Fatals the specified message. - /// - /// The logger. - /// The message. - /// The exception. - public static void Fatal(this ILogger logger, string message, Exception exception) - { - logger.Log(new LogEntry(LoggingEventType.Fatal, message, exception)); - } - - /// - /// Fatals the specified format provider. - /// - /// The logger. - /// The format provider. - /// The format. - /// The arguments. - public static void Fatal(this ILogger logger, IFormatProvider formatProvider, string format, params object[] args) - { - logger.Log(new LogEntry(LoggingEventType.Fatal, string.Format(formatProvider, format, args))); - } - - /// - /// Fatals the specified format. - /// - /// The logger. - /// The format. - /// The arguments. - public static void Fatal(this ILogger logger, string format, params object[] args) - { - logger.Log(new LogEntry(LoggingEventType.Fatal, string.Format(CultureInfo.CurrentCulture, format, args))); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Logging/RedmineConsoleTraceListener.cs b/redmine-net20-api/Logging/RedmineConsoleTraceListener.cs deleted file mode 100755 index 48404d74..00000000 --- a/redmine-net20-api/Logging/RedmineConsoleTraceListener.cs +++ /dev/null @@ -1,86 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Diagnostics; - -namespace Redmine.Net.Api.Logging -{ - /// - /// - /// - public class RedmineConsoleTraceListener : TraceListener - { - #region implemented abstract members of TraceListener - - /// - /// When overridden in a derived class, writes the specified message to the listener you create in the derived class. - /// - /// A message to write. - public override void Write (string message) - { - Console.Write(message); - } - - /// - /// When overridden in a derived class, writes a message to the listener you create in the derived class, followed by a line terminator. - /// - /// A message to write. - public override void WriteLine (string message) - { - Console.WriteLine(message); - } - - #endregion - - /// - /// Writes trace information, a message, and event information to the listener specific output. - /// - /// A object that contains the current process ID, thread ID, and stack trace information. - /// A name used to identify the output, typically the name of the application that generated the trace event. - /// One of the values specifying the type of event that has caused the trace. - /// A numeric identifier for the event. - /// A message to write. - /// - /// - /// - /// - public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, - string message) - { - WriteLine(message); - } - - /// - /// Writes trace information, a formatted array of objects and event information to the listener specific output. - /// - /// A object that contains the current process ID, thread ID, and stack trace information. - /// A name used to identify the output, typically the name of the application that generated the trace event. - /// One of the values specifying the type of event that has caused the trace. - /// A numeric identifier for the event. - /// A format string that contains zero or more format items, which correspond to objects in the array. - /// An object array containing zero or more objects to format. - /// - /// - /// - /// - public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, - string format, params object[] args) - { - WriteLine(string.Format(format, args)); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Logging/TraceLogger.cs b/redmine-net20-api/Logging/TraceLogger.cs deleted file mode 100755 index 902b552d..00000000 --- a/redmine-net20-api/Logging/TraceLogger.cs +++ /dev/null @@ -1,56 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Diagnostics; - -namespace Redmine.Net.Api.Logging -{ - /// - /// - /// - public class TraceLogger : ILogger - { - /// - /// Logs the specified entry. - /// - /// The entry. - /// - public void Log(LogEntry entry) - { - switch (entry.Severity) - { - case LoggingEventType.Debug: - Trace.WriteLine(entry.Message, "Debug"); - break; - case LoggingEventType.Information: - Trace.TraceInformation(entry.Message); - break; - case LoggingEventType.Warning: - Trace.TraceWarning(entry.Message); - break; - case LoggingEventType.Error: - Trace.TraceError(entry.Message); - break; - case LoggingEventType.Fatal: - Trace.WriteLine(entry.Message, "Fatal"); - break; - default: - throw new ArgumentOutOfRangeException(); - } - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/MimeFormat.cs b/redmine-net20-api/MimeFormat.cs deleted file mode 100644 index 803c896d..00000000 --- a/redmine-net20-api/MimeFormat.cs +++ /dev/null @@ -1,28 +0,0 @@ -ο»Ώ/* -Copyright 2011 - 2017 Adrian Popescu. - -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. -*/ - -namespace Redmine.Net.Api -{ - /// - /// - /// - public enum MimeFormat - { - /// - /// - Xml - } -} \ No newline at end of file diff --git a/redmine-net20-api/RedmineManager.cs b/redmine-net20-api/RedmineManager.cs deleted file mode 100644 index 111e21a7..00000000 --- a/redmine-net20-api/RedmineManager.cs +++ /dev/null @@ -1,755 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; -using System.Net; -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Text.RegularExpressions; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Logging; -using Redmine.Net.Api.Types; -using Group = Redmine.Net.Api.Types.Group; -using Version = Redmine.Net.Api.Types.Version; - -namespace Redmine.Net.Api -{ - /// - /// The main class to access Redmine API. - /// - public class RedmineManager - { - /// - /// - public const int DEFAULT_PAGE_SIZE_VALUE = 25; - - private static readonly Dictionary routes = new Dictionary - { - {typeof(Issue), "issues"}, - {typeof(Project), "projects"}, - {typeof(User), "users"}, - {typeof(News), "news"}, - {typeof(Query), "queries"}, - {typeof(Version), "versions"}, - {typeof(Attachment), "attachments"}, - {typeof(IssueRelation), "relations"}, - {typeof(TimeEntry), "time_entries"}, - {typeof(IssueStatus), "issue_statuses"}, - {typeof(Tracker), "trackers"}, - {typeof(IssueCategory), "issue_categories"}, - {typeof(Role), "roles"}, - {typeof(ProjectMembership), "memberships"}, - {typeof(Group), "groups"}, - {typeof(TimeEntryActivity), "enumerations/time_entry_activities"}, - {typeof(IssuePriority), "enumerations/issue_priorities"}, - {typeof(Watcher), "watchers"}, - {typeof(IssueCustomField), "custom_fields"}, - {typeof(CustomField), "custom_fields"} - }; - - private readonly string basicAuthorization; - private readonly CredentialCache cache; - private string host; - - /// - /// Initializes a new instance of the class. - /// - /// The host. - /// The MIME format. - /// if set to true [verify server cert]. - /// The proxy. - /// Use this parameter to specify a SecurityProtcolType. Note: it is recommended to leave this parameter at its default value as this setting also affects the calling application process. - /// - /// Host is not defined! - /// or - /// The host is not valid! - /// - public RedmineManager(string host, MimeFormat mimeFormat = MimeFormat.Xml, bool verifyServerCert = true, - IWebProxy proxy = null, SecurityProtocolType securityProtocolType = default(SecurityProtocolType)) - { - if (string.IsNullOrEmpty(host)) throw new RedmineException("Host is not defined!"); - PageSize = 25; - - if (default(SecurityProtocolType) == securityProtocolType) - { - securityProtocolType = ServicePointManager.SecurityProtocol; - } - - Host = host; - MimeFormat = mimeFormat; - Proxy = proxy; - SecurityProtocolType = securityProtocolType; - - ServicePointManager.SecurityProtocol = securityProtocolType; - if (!verifyServerCert) - { - ServicePointManager.ServerCertificateValidationCallback += RemoteCertValidate; - } - } - - /// - /// Initializes a new instance of the class. - /// Most of the time, the API requires authentication. To enable the API-style authentication, you have to check Enable - /// REST API in Administration -> Settings -> Authentication. Then, authentication can be done in 2 different - /// ways: - /// using your regular login/password via HTTP Basic authentication. - /// using your API key which is a handy way to avoid putting a password in a script. The API key may be attached to - /// each request in one of the following way: - /// passed in as a "key" parameter - /// passed in as a username with a random password via HTTP Basic authentication - /// passed in as a "X-Redmine-API-Key" HTTP header (added in Redmine 1.1.0) - /// You can find your API key on your account page ( /my/account ) when logged in, on the right-hand pane of the - /// default layout. - /// - /// The host. - /// The API key. - /// The MIME format. - /// if set to true [verify server cert]. - /// The proxy. - /// Use this parameter to specify a SecurityProtcolType. Note: it is recommended to leave this parameter at its default value as this setting also affects the calling application process. - public RedmineManager(string host, string apiKey, MimeFormat mimeFormat = MimeFormat.Xml, - bool verifyServerCert = true, IWebProxy proxy = null, - SecurityProtocolType securityProtocolType = default(SecurityProtocolType)) - : this(host, mimeFormat, verifyServerCert, proxy, securityProtocolType) - { - ApiKey = apiKey; - } - - /// - /// Initializes a new instance of the class. - /// Most of the time, the API requires authentication. To enable the API-style authentication, you have to check Enable - /// REST API in Administration -> Settings -> Authentication. Then, authentication can be done in 2 different - /// ways: - /// using your regular login/password via HTTP Basic authentication. - /// using your API key which is a handy way to avoid putting a password in a script. The API key may be attached to - /// each request in one of the following way: - /// passed in as a "key" parameter - /// passed in as a username with a random password via HTTP Basic authentication - /// passed in as a "X-Redmine-API-Key" HTTP header (added in Redmine 1.1.0) - /// You can find your API key on your account page ( /my/account ) when logged in, on the right-hand pane of the - /// default layout. - /// - /// The host. - /// The login. - /// The password. - /// The MIME format. - /// if set to true [verify server cert]. - /// The proxy. - /// Use this parameter to specify a SecurityProtcolType. Note: it is recommended to leave this parameter at its default value as this setting also affects the calling application process. - public RedmineManager(string host, string login, string password, MimeFormat mimeFormat = MimeFormat.Xml, - bool verifyServerCert = true, IWebProxy proxy = null, - SecurityProtocolType securityProtocolType = default(SecurityProtocolType)) - : this(host, mimeFormat, verifyServerCert, proxy, securityProtocolType) - { - cache = new CredentialCache { { new Uri(host), "Basic", new NetworkCredential(login, password) } }; - - var token = Convert.ToBase64String(Encoding.UTF8.GetBytes(string.Format("{0}:{1}", login, password))); - basicAuthorization = string.Format("Basic {0}", token); - } - - /// - /// Gets the sufixes. - /// - /// - /// The sufixes. - /// - public static Dictionary Sufixes - { - get { return routes; } - } - - /// - /// Gets the host. - /// - /// - /// The host. - /// - public string Host - { - get { return host; } - private set - { - host = value; - - Uri uriResult; - if (!Uri.TryCreate(host, UriKind.Absolute, out uriResult) || - !(uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps)) - { - host = "http://" + host; - } - - if (!Uri.TryCreate(host, UriKind.Absolute, out uriResult)) - throw new RedmineException("The host is not valid!"); - } - } - - /// - /// The ApiKey used to authenticate. - /// - /// - /// The API key. - /// - public string ApiKey { get; private set; } - - /// - /// Maximum page-size when retrieving complete object lists - /// - /// By default only 25 results can be retrieved per request. Maximum is 100. To change the maximum value set - /// in your Settings -> General, "Objects per page options".By adding (for instance) 9999 there would make you - /// able to get that many results per request. - /// - /// - /// - /// The size of the page. - /// - public int PageSize { get; set; } - - /// - /// As of Redmine 2.2.0 you can impersonate user setting user login (eg. jsmith). This only works when using the API - /// with an administrator account, this header will be ignored when using the API with a regular user account. - /// - /// - /// The impersonate user. - /// - public string ImpersonateUser { get; set; } - - /// - /// Gets the MIME format. - /// - /// - /// The MIME format. - /// - public MimeFormat MimeFormat { get; private set; } - - /// - /// Gets the proxy. - /// - /// - /// The proxy. - /// - public IWebProxy Proxy { get; private set; } - - /// - /// Gets the type of the security protocol. - /// - /// - /// The type of the security protocol. - /// - public SecurityProtocolType SecurityProtocolType { get; private set; } - - /// - /// Returns the user whose credentials are used to access the API. - /// - /// The accepted parameters are: memberships and groups (added in 2.1). - /// - /// - /// An error occurred during deserialization. The original exception is available - /// using the System.Exception.InnerException property. - /// - public User GetCurrentUser(NameValueCollection parameters = null) - { - var url = UrlHelper.GetCurrentUserUrl(this); - return WebApiHelper.ExecuteDownload(this, url, "GetCurrentUser", parameters); - } - - /// - /// Adds the watcher to issue. - /// - /// The issue identifier. - /// The user identifier. - public void AddWatcherToIssue(int issueId, int userId) - { - var url = UrlHelper.GetAddWatcherUrl(this, issueId, userId); - WebApiHelper.ExecuteUpload(this, url, HttpVerbs.POST, DataHelper.UserData(userId, MimeFormat), "AddWatcher"); - } - - /// - /// Removes the watcher from issue. - /// - /// The issue identifier. - /// The user identifier. - public void RemoveWatcherFromIssue(int issueId, int userId) - { - var url = UrlHelper.GetRemoveWatcherUrl(this, issueId, userId); - WebApiHelper.ExecuteUpload(this, url, HttpVerbs.DELETE, string.Empty, "RemoveWatcher"); - } - - /// - /// Adds an existing user to a group. - /// - /// The group id. - /// The user id. - public void AddUserToGroup(int groupId, int userId) - { - var url = UrlHelper.GetAddUserToGroupUrl(this, groupId); - WebApiHelper.ExecuteUpload(this, url, HttpVerbs.POST, DataHelper.UserData(userId, MimeFormat), "AddUser"); - } - - /// - /// Removes an user from a group. - /// - /// The group id. - /// The user id. - public void RemoveUserFromGroup(int groupId, int userId) - { - var url = UrlHelper.GetRemoveUserFromGroupUrl(this, groupId, userId); - WebApiHelper.ExecuteUpload(this, url, HttpVerbs.DELETE, string.Empty, "DeleteUser"); - } - - /// - /// Creates or updates a wiki page. - /// - /// The project id or identifier. - /// The wiki page name. - /// The wiki page to create or update. - /// - public WikiPage CreateOrUpdateWikiPage(string projectId, string pageName, WikiPage wikiPage) - { - var result = RedmineSerializer.Serialize(wikiPage, MimeFormat); - if (string.IsNullOrEmpty(result)) return null; - - var url = UrlHelper.GetWikiCreateOrUpdaterUrl(this, projectId, pageName); - return WebApiHelper.ExecuteUpload(this, url, HttpVerbs.PUT, result, "CreateOrUpdateWikiPage"); - } - - /// - /// Gets the wiki page. - /// - /// The project identifier. - /// The parameters. - /// Name of the page. - /// The version. - /// - public WikiPage GetWikiPage(string projectId, NameValueCollection parameters, string pageName, uint version = 0) - { - var url = UrlHelper.GetWikiPageUrl(this, projectId, parameters, pageName, version); - return WebApiHelper.ExecuteDownload(this, url, "GetWikiPage", parameters); - } - - /// - /// Returns the list of all pages in a project wiki. - /// - /// The project id or identifier. - /// - public List GetAllWikiPages(string projectId) - { - var url = UrlHelper.GetWikisUrl(this, projectId); - var result = WebApiHelper.ExecuteDownloadList(this, url, "GetAllWikiPages"); - return result != null ? result.Objects : null; - } - - /// - /// Deletes a wiki page, its attachments and its history. If the deleted page is a parent page, its child pages are not - /// deleted but changed as root pages. - /// - /// The project id or identifier. - /// The wiki page name. - public void DeleteWikiPage(string projectId, string pageName) - { - var url = UrlHelper.GetDeleteWikirUrl(this, projectId, pageName); - WebApiHelper.ExecuteUpload(this, url, HttpVerbs.DELETE, string.Empty, "DeleteWikiPage"); - } - - /// - /// Gets the redmine object based on id. - /// - /// The type of objects to retrieve. - /// The id of the object. - /// Optional filters and/or optional fetched data. - /// - /// Returns the object of type T. - /// - /// - /// An error occurred during deserialization. The original exception is available - /// using the System.Exception.InnerException property. - /// - /// - /// - /// string issueId = "927"; - /// NameValueCollection parameters = null; - /// Issue issue = redmineManager.GetObject<Issue>(issueId, parameters); - /// - /// - public T GetObject(string id, NameValueCollection parameters) where T : class, new() - { - var url = UrlHelper.GetGetUrl(this, id); - return WebApiHelper.ExecuteDownload(this, url, "GetObject", parameters); - } - - /// - /// Gets the paginated objects. - /// - /// - /// The parameters. - /// - public PaginatedObjects GetPaginatedObjects(NameValueCollection parameters) where T : class, new() - { - var url = UrlHelper.GetListUrl(this, parameters); - return WebApiHelper.ExecuteDownloadList(this, url, "GetObjectList", parameters); - } - - /// - /// Returns the complete list of objects. - /// - /// - /// The page size. - /// The offset. - /// Optional fetched data. - /// - /// Optional fetched data: - /// Project: trackers, issue_categories, enabled_modules (since 2.6.0) - /// Issue: children, attachments, relations, changesets, journals, watchers - Since 2.3.0 - /// Users: memberships, groups (added in 2.1) - /// Groups: users, memberships - /// - /// Returns the complete list of objects. - public List GetObjects(int limit, int offset, params string[] include) where T : class, new() - { - var parameters = new NameValueCollection(); - parameters.Add(RedmineKeys.LIMIT, limit.ToString(CultureInfo.InvariantCulture)); - parameters.Add(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); - if (include != null) - { - parameters.Add(RedmineKeys.INCLUDE, string.Join(",", include)); - } - - return GetObjects(parameters); - } - - /// - /// Returns the complete list of objects. - /// - /// - /// Optional fetched data. - /// - /// Optional fetched data: - /// Project: trackers, issue_categories, enabled_modules (since 2.6.0) - /// Issue: children, attachments, relations, changesets, journals, watchers - Since 2.3.0 - /// Users: memberships, groups (added in 2.1) - /// Groups: users, memberships - /// - /// Returns the complete list of objects. - public List GetObjects(params string[] include) where T : class, new() - { - return GetObjects(PageSize, 0, include); - } - - /// - /// Returns the complete list of objects. - /// - /// The type of objects to retrieve. - /// Optional filters and/or optional fetched data. - /// - /// Returns a complete list of objects. - /// - public List GetObjects(NameValueCollection parameters) where T : class, new() - { - int totalCount = 0, pageSize = 0, offset = 0; - var isLimitSet = false; - List resultList = null; - - if (parameters == null) - { - parameters = new NameValueCollection(); - } - else - { - isLimitSet = int.TryParse(parameters[RedmineKeys.LIMIT], out pageSize); - int.TryParse(parameters[RedmineKeys.OFFSET], out offset); - } - if (pageSize == default(int)) - { - pageSize = PageSize > 0 ? PageSize : DEFAULT_PAGE_SIZE_VALUE; - parameters.Set(RedmineKeys.LIMIT, pageSize.ToString(CultureInfo.InvariantCulture)); - } - - try - { - do - { - parameters.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); - var tempResult = GetPaginatedObjects(parameters); - if (tempResult != null) - { - if (resultList == null) - { - resultList = tempResult.Objects; - totalCount = isLimitSet ? pageSize : tempResult.TotalCount; - } - else - { - resultList.AddRange(tempResult.Objects); - } - } - offset += pageSize; - } while (offset < totalCount); - } - catch (WebException wex) - { - wex.HandleWebException("GetObjectsAsync", MimeFormat); - } - return resultList; - } - - /// - /// Creates a new Redmine object. - /// - /// The type of object to create. - /// The object to create. - /// The owner identifier. - /// - /// - /// - /// When trying to create an object with invalid or missing attribute parameters, you will get a 422 Unprocessable - /// Entity response. That means that the object could not be created. - /// - /// - /// - /// var project = new Project(); - /// project.Name = "test"; - /// project.Identifier = "the project identifier"; - /// project.Description = "the project description"; - /// redmineManager.CreateObject(project); - /// - /// - public T CreateObject(T obj, string ownerId) where T : class, new() - { - var url = UrlHelper.GetCreateUrl(this, ownerId); - var data = RedmineSerializer.Serialize(obj, MimeFormat); - return WebApiHelper.ExecuteUpload(this, url, HttpVerbs.POST, data, "CreateObject"); - } - - /// - /// Updates a Redmine object. - /// - /// The type of object to be update. - /// The id of the object to be update. - /// The object to be update. - /// - /// - /// When trying to update an object with invalid or missing attribute parameters, you will get a 422(RedmineException) - /// Unprocessable Entity response. That means that the object could not be updated. - /// - /// - public void UpdateObject(string id, T obj) where T : class, new() - { - UpdateObject(id, obj, null); - } - - /// - /// Updates a Redmine object. - /// - /// The type of object to be update. - /// The id of the object to be update. - /// The object to be update. - /// The project identifier. - /// - /// - /// When trying to update an object with invalid or missing attribute parameters, you will get a - /// 422(RedmineException) Unprocessable Entity response. That means that the object could not be updated. - /// - /// - public void UpdateObject(string id, T obj, string projectId) where T : class, new() - { - var url = UrlHelper.GetUploadUrl(this, id, obj, projectId); - var data = RedmineSerializer.Serialize(obj, MimeFormat); - data = Regex.Replace(data, @"\r\n|\r|\n", "\r\n"); - WebApiHelper.ExecuteUpload(this, url, HttpVerbs.PUT, data, "UpdateObject"); - } - - /// - /// Deletes the Redmine object. - /// - /// The type of objects to delete. - /// The id of the object to delete - /// - /// - public void DeleteObject(string id) where T : class, new() - { - DeleteObject(id, null); - } - - /// - /// Deletes the Redmine object. - /// - /// The type of objects to delete. - /// The id of the object to delete - /// The parameters - /// - /// - public void DeleteObject(string id, NameValueCollection parameters = null) where T : class, new() - { - var url = UrlHelper.GetDeleteUrl(this, id); - WebApiHelper.ExecuteUpload(this, url, HttpVerbs.DELETE, string.Empty, "DeleteObject", parameters); - } - - /// - /// Support for adding attachments through the REST API is added in Redmine 1.4.0. - /// Upload a file to server. - /// - /// The content of the file that will be uploaded on server. - /// - /// Returns the token for uploaded file. - /// - /// - /// - /// - /// - /// - /// - /// - public Upload UploadFile(byte[] data) - { - var url = UrlHelper.GetUploadFileUrl(this); - return WebApiHelper.ExecuteUploadFile(this, url, data, "UploadFile"); - } - - /// - /// Updates the attachment. - /// - /// The issue identifier. - /// The attachment. - public void UpdateAttachment(int issueId, Attachment attachment) - { - var address = UrlHelper.GetAttachmentUpdateUrl(this, issueId); - var attachments = new Attachments { { attachment.Id, attachment } }; - var data = RedmineSerializer.Serialize(attachments, MimeFormat); - - WebApiHelper.ExecuteUpload(this, address, HttpVerbs.PATCH, data, "UpdateAttachment"); - } - - /// - /// Downloads the file. - /// - /// The address. - /// - /// - /// - /// - /// - /// - /// - /// - /// - public byte[] DownloadFile(string address) - { - return WebApiHelper.ExecuteDownloadFile(this, address, "DownloadFile"); - } - - /// - /// Creates a new Redmine object. - /// - /// The type of object to create. - /// The object to create. - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// When trying to create an object with invalid or missing attribute parameters, you will get a 422 Unprocessable - /// Entity response. That means that the object could not be created. - /// - public T CreateObject(T obj) where T : class, new() - { - return CreateObject(obj, null); - } - - /// - /// Creates the Redmine web client. - /// - /// The parameters. - /// if set to true [upload file]. - /// - /// - public virtual RedmineWebClient CreateWebClient(NameValueCollection parameters, bool uploadFile = false) - { - var webClient = new RedmineWebClient { Proxy = Proxy }; - if (!uploadFile) - { - webClient.Headers.Add(HttpRequestHeader.ContentType, MimeFormat == MimeFormat.Xml - ? "application/xml" - : "application/json"); - webClient.Encoding = Encoding.UTF8; - } - else - { - webClient.Headers.Add(HttpRequestHeader.ContentType, "application/octet-stream"); - } - - if (parameters != null) - { - webClient.QueryString = parameters; - } - - if (!string.IsNullOrEmpty(ApiKey)) - { - webClient.QueryString[RedmineKeys.KEY] = ApiKey; - } - else - { - if (cache != null) - { - webClient.PreAuthenticate = true; - webClient.Credentials = cache; - webClient.Headers[HttpRequestHeader.Authorization] = basicAuthorization; - } - else - { - webClient.UseDefaultCredentials = true; - webClient.Credentials = CredentialCache.DefaultCredentials; - } - } - - if (!string.IsNullOrEmpty(ImpersonateUser)) - { - webClient.Headers.Add("X-Redmine-Switch-User", ImpersonateUser); - } - - return webClient; - } - - /// - /// This is to take care of SSL certification validation which are not issued by Trusted Root CA. - /// - /// The sender. - /// The cert. - /// The chain. - /// The error. - /// - /// - public virtual bool RemoteCertValidate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors error) - { - if (error == SslPolicyErrors.None) - { - return true; - } - - Logger.Current.Error("X509Certificate [{0}] Policy Error: '{1}'", cert.Subject, error); - - return false; - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/RedmineWebClient.cs b/redmine-net20-api/RedmineWebClient.cs deleted file mode 100755 index a544b0bd..00000000 --- a/redmine-net20-api/RedmineWebClient.cs +++ /dev/null @@ -1,123 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Net; - -namespace Redmine.Net.Api -{ - /// - /// - /// - public class RedmineWebClient : WebClient - { - private const string UA = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:8.0) Gecko/20100101 Firefox/8.0"; - - /// - /// Gets or sets a value indicating whether [use proxy]. - /// - /// - /// true if [use proxy]; otherwise, false. - /// - public bool UseProxy { get; set; } - - /// - /// Gets or sets a value indicating whether [use cookies]. - /// - /// - /// true if [use cookies]; otherwise, false. - /// - public bool UseCookies { get; set; } - - /// - /// in miliseconds - /// - /// - /// The timeout. - /// - public TimeSpan? Timeout { get; set; } - - /// - /// Gets or sets the cookie container. - /// - /// - /// The cookie container. - /// - public CookieContainer CookieContainer { get; set; } - - /// - /// Gets or sets a value indicating whether [pre authenticate]. - /// - /// - /// true if [pre authenticate]; otherwise, false. - /// - public bool PreAuthenticate { get; set; } - - /// - /// Gets or sets a value indicating whether [keep alive]. - /// - /// - /// true if [keep alive]; otherwise, false. - /// - public bool KeepAlive { get; set; } - - /// - /// Returns a object for the specified resource. - /// - /// A that identifies the resource to request. - /// - /// A new object for the specified resource. - /// - protected override WebRequest GetWebRequest(Uri address) - { - var wr = base.GetWebRequest(address); - var httpWebRequest = wr as HttpWebRequest; - - if (httpWebRequest != null) - { - if (UseCookies) - { - httpWebRequest.Headers.Add(HttpRequestHeader.Cookie, "redmineCookie"); - httpWebRequest.CookieContainer = CookieContainer; - } - httpWebRequest.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | - DecompressionMethods.None; - httpWebRequest.PreAuthenticate = PreAuthenticate; - httpWebRequest.KeepAlive = KeepAlive; - httpWebRequest.UseDefaultCredentials = UseDefaultCredentials; - httpWebRequest.Credentials = Credentials; - httpWebRequest.UserAgent = UA; - httpWebRequest.CachePolicy = CachePolicy; - - if (UseProxy) - { - if (Proxy != null) - { - Proxy.Credentials = Credentials; - } - httpWebRequest.Proxy = Proxy; - } - - if (Timeout != null) - httpWebRequest.Timeout = Timeout.Value.Milliseconds; - - return httpWebRequest; - } - - return base.GetWebRequest(address); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/Attachment.cs b/redmine-net20-api/Types/Attachment.cs deleted file mode 100755 index 9559a51d..00000000 --- a/redmine-net20-api/Types/Attachment.cs +++ /dev/null @@ -1,180 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 1.3 - /// - [XmlRoot(RedmineKeys.ATTACHMENT)] - public class Attachment : Identifiable, IXmlSerializable, IEquatable - { - /// - /// Gets or sets the name of the file. - /// - /// The name of the file. - [XmlElement(RedmineKeys.FILENAME)] - public String FileName { get; set; } - - /// - /// Gets or sets the size of the file. - /// - /// The size of the file. - [XmlElement(RedmineKeys.FILESIZE)] - public int FileSize { get; set; } - - /// - /// Gets or sets the type of the content. - /// - /// The type of the content. - [XmlElement(RedmineKeys.CONTENT_TYPE)] - public String ContentType { get; set; } - - /// - /// Gets or sets the description. - /// - /// The description. - [XmlElement(RedmineKeys.DESCRIPTION)] - public String Description { get; set; } - - /// - /// Gets or sets the content URL. - /// - /// The content URL. - [XmlElement(RedmineKeys.CONTENT_URL)] - public String ContentUrl { get; set; } - - /// - /// Gets or sets the author. - /// - /// The author. - [XmlElement(RedmineKeys.AUTHOR)] - public IdentifiableName Author { get; set; } - - /// - /// Gets or sets the created on. - /// - /// The created on. - [XmlElement(RedmineKeys.CREATED_ON)] - public DateTime? CreatedOn { get; set; } - - /// - /// - /// - /// - public XmlSchema GetSchema() { return null; } - - /// - /// - /// - /// - public void ReadXml(XmlReader reader) - { - reader.Read(); - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.FILENAME: FileName = reader.ReadElementContentAsString(); break; - - case RedmineKeys.FILESIZE: FileSize = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.CONTENT_TYPE: ContentType = reader.ReadElementContentAsString(); break; - - case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; - - case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; - - case RedmineKeys.CONTENT_URL: ContentUrl = reader.ReadElementContentAsString(); break; - - default: reader.Read(); break; - } - } - } - - /// - /// - /// - /// - public void WriteXml(XmlWriter writer) { } - - /// - /// - /// - /// - /// - public bool Equals(Attachment other) - { - if (other == null) return false; - return (Id == other.Id - && FileName == other.FileName - && FileSize == other.FileSize - && ContentType == other.ContentType - && Author == other.Author - && CreatedOn == other.CreatedOn - && Description == other.Description - && ContentUrl == other.ContentUrl); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(FileName, hashCode); - hashCode = HashCodeHelper.GetHashCode(FileSize, hashCode); - hashCode = HashCodeHelper.GetHashCode(ContentType, hashCode); - hashCode = HashCodeHelper.GetHashCode(Author, hashCode); - hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(Description, hashCode); - hashCode = HashCodeHelper.GetHashCode(ContentUrl, hashCode); - - return hashCode; - } - } - - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[Attachment: {7}, FileName={0}, FileSize={1}, ContentType={2}, Description={3}, ContentUrl={4}, Author={5}, CreatedOn={6}]", - FileName, FileSize, ContentType, Description, ContentUrl, Author, CreatedOn, base.ToString()); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/CustomField.cs b/redmine-net20-api/Types/CustomField.cs deleted file mode 100755 index f89b5ce7..00000000 --- a/redmine-net20-api/Types/CustomField.cs +++ /dev/null @@ -1,258 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Xml; -using System.Xml.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// - /// - [XmlRoot(RedmineKeys.CUSTOM_FIELD)] - public class CustomField : IdentifiableName, IEquatable - { - /// - /// - /// - [XmlElement(RedmineKeys.CUSTOMIZED_TYPE)] - public string CustomizedType { get; set; } - - /// - /// - /// - [XmlElement(RedmineKeys.FIELD_FORMAT)] - public string FieldFormat { get; set; } - - /// - /// - /// - [XmlElement(RedmineKeys.REGEXP)] - public string Regexp { get; set; } - - /// - /// - /// - [XmlElement(RedmineKeys.MIN_LENGTH)] - public int? MinLength { get; set; } - - /// - /// - /// - [XmlElement(RedmineKeys.MAX_LENGTH)] - public int? MaxLength { get; set; } - - /// - /// - /// - [XmlElement(RedmineKeys.IS_REQUIRED)] - public bool IsRequired { get; set; } - - /// - /// - /// - [XmlElement(RedmineKeys.IS_FILTER)] - public bool IsFilter { get; set; } - - /// - /// - /// - [XmlElement(RedmineKeys.SEARCHABLE)] - public bool Searchable { get; set; } - - /// - /// - /// - [XmlElement(RedmineKeys.MULTIPLE)] - public bool Multiple { get; set; } - - /// - /// - /// - [XmlElement(RedmineKeys.DEFAULT_VALUE)] - public string DefaultValue { get; set; } - - /// - /// - /// - [XmlElement(RedmineKeys.VISIBLE)] - public bool Visible { get; set; } - - /// - /// - /// - [XmlArray(RedmineKeys.POSSIBLE_VALUES)] - [XmlArrayItem(RedmineKeys.POSSIBLE_VALUE)] - public IList PossibleValues { get; set; } - - /// - /// - /// - [XmlArray(RedmineKeys.TRACKERS)] - [XmlArrayItem(RedmineKeys.TRACKER)] - public IList Trackers { get; set; } - - /// - /// - /// - [XmlArray(RedmineKeys.ROLES)] - [XmlArrayItem(RedmineKeys.ROLE)] - public IList Roles { get; set; } - - /// - /// - /// - /// - public override void ReadXml(XmlReader reader) - { - reader.Read(); - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; - - case RedmineKeys.CUSTOMIZED_TYPE: CustomizedType = reader.ReadElementContentAsString(); break; - - case RedmineKeys.FIELD_FORMAT: FieldFormat = reader.ReadElementContentAsString(); break; - - case RedmineKeys.REGEXP: Regexp = reader.ReadElementContentAsString(); break; - - case RedmineKeys.MIN_LENGTH: MinLength = reader.ReadElementContentAsNullableInt(); break; - - case RedmineKeys.MAX_LENGTH: MaxLength = reader.ReadElementContentAsNullableInt(); break; - - case RedmineKeys.IS_REQUIRED: IsRequired = reader.ReadElementContentAsBoolean(); break; - - case RedmineKeys.IS_FILTER: IsFilter = reader.ReadElementContentAsBoolean(); break; - - case RedmineKeys.SEARCHABLE: Searchable = reader.ReadElementContentAsBoolean(); break; - - case RedmineKeys.VISIBLE: Visible = reader.ReadElementContentAsBoolean(); break; - - case RedmineKeys.DEFAULT_VALUE: DefaultValue = reader.ReadElementContentAsString(); break; - - case RedmineKeys.MULTIPLE: Multiple = reader.ReadElementContentAsBoolean(); break; - - case RedmineKeys.TRACKERS: Trackers = reader.ReadElementContentAsCollection(); break; - - case RedmineKeys.ROLES: Roles = reader.ReadElementContentAsCollection(); break; - - case RedmineKeys.POSSIBLE_VALUES: PossibleValues = reader.ReadElementContentAsCollection(); break; - - default: reader.Read(); break; - } - } - } - - /// - /// - /// - /// - public override void WriteXml(XmlWriter writer) { } - - /// - /// - /// - /// - /// - public bool Equals(CustomField other) - { - if (other == null) return false; - - return Id == other.Id - && IsFilter == other.IsFilter - && IsRequired == other.IsRequired - && Multiple == other.Multiple - && Searchable == other.Searchable - && Visible == other.Visible - && CustomizedType.Equals(other.CustomizedType) - && DefaultValue.Equals(other.DefaultValue) - && FieldFormat.Equals(other.FieldFormat) - && MaxLength == other.MaxLength - && MinLength == other.MinLength - && Name.Equals(other.Name) - && Regexp.Equals(other.Regexp) - && PossibleValues.Equals(other.PossibleValues) - && Roles.Equals(other.Roles) - && Trackers.Equals(other.Trackers); - } - - /// - /// - /// - /// - /// - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals(obj as CustomField); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id,hashCode); - hashCode = HashCodeHelper.GetHashCode(IsFilter,hashCode); - hashCode = HashCodeHelper.GetHashCode(IsRequired,hashCode); - hashCode = HashCodeHelper.GetHashCode(Multiple,hashCode); - hashCode = HashCodeHelper.GetHashCode(Searchable,hashCode); - hashCode = HashCodeHelper.GetHashCode(Visible,hashCode); - hashCode = HashCodeHelper.GetHashCode(CustomizedType,hashCode); - hashCode = HashCodeHelper.GetHashCode(DefaultValue,hashCode); - hashCode = HashCodeHelper.GetHashCode(FieldFormat,hashCode); - hashCode = HashCodeHelper.GetHashCode(MaxLength,hashCode); - hashCode = HashCodeHelper.GetHashCode(MinLength,hashCode); - hashCode = HashCodeHelper.GetHashCode(Name,hashCode); - hashCode = HashCodeHelper.GetHashCode(Regexp,hashCode); - hashCode = HashCodeHelper.GetHashCode(PossibleValues,hashCode); - hashCode = HashCodeHelper.GetHashCode(Roles,hashCode); - hashCode = HashCodeHelper.GetHashCode(Trackers,hashCode); - return hashCode; - } - } - - /// - /// - /// - /// - public override string ToString () - { - return string.Format ("[CustomField: Id={0}, Name={1}, CustomizedType={2}, FieldFormat={3}, Regexp={4}, MinLength={5}, MaxLength={6}, IsRequired={7}, IsFilter={8}, Searchable={9}, Multiple={10}, DefaultValue={11}, Visible={12}, PossibleValues={13}, Trackers={14}, Roles={15}]", - Id, Name, CustomizedType, FieldFormat, Regexp, MinLength, MaxLength, IsRequired, IsFilter, Searchable, Multiple, DefaultValue, Visible, PossibleValues, Trackers, Roles); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/CustomFieldPossibleValue.cs b/redmine-net20-api/Types/CustomFieldPossibleValue.cs deleted file mode 100755 index a78fbe08..00000000 --- a/redmine-net20-api/Types/CustomFieldPossibleValue.cs +++ /dev/null @@ -1,82 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Xml.Serialization; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// - /// - [XmlRoot(RedmineKeys.POSSIBLE_VALUE)] - public class CustomFieldPossibleValue : IEquatable - { - /// - /// - /// - [XmlElement(RedmineKeys.VALUE)] - public string Value { get; set; } - - /// - /// - /// - /// - /// - public bool Equals(CustomFieldPossibleValue other) - { - if (other == null) return false; - return (Value == other.Value); - } - - /// - /// - /// - /// - /// - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals(obj as CustomFieldPossibleValue); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Value,hashCode); - return hashCode; - } - } - - /// - /// - /// - /// - public override string ToString () - { - return string.Format ("[CustomFieldPossibleValue: {0}]", base.ToString()); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/CustomFieldValue.cs b/redmine-net20-api/Types/CustomFieldValue.cs deleted file mode 100755 index f87f0a78..00000000 --- a/redmine-net20-api/Types/CustomFieldValue.cs +++ /dev/null @@ -1,91 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Xml.Serialization; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// - /// - [XmlRoot(RedmineKeys.VALUE)] - public class CustomFieldValue : IEquatable, ICloneable - { - /// - /// - /// - [XmlText] - public string Info { get; set; } - - /// - /// - /// - /// - /// - public bool Equals(CustomFieldValue other) - { - return Info.Equals(other.Info); - } - - /// - /// - /// - /// - /// - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals(obj as CustomFieldValue); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Info, hashCode); - return hashCode; - } - } - - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[CustomFieldValue: Info={0}]", Info); - } - - /// - /// - /// - /// - public object Clone() - { - var customFieldValue = new CustomFieldValue { Info = Info }; - return customFieldValue; - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/Detail.cs b/redmine-net20-api/Types/Detail.cs deleted file mode 100755 index cda0232d..00000000 --- a/redmine-net20-api/Types/Detail.cs +++ /dev/null @@ -1,163 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// - /// - [XmlRoot(RedmineKeys.DETAIL)] - public class Detail : IXmlSerializable, IEquatable - { - /// - /// Gets or sets the property. - /// - /// - /// The property. - /// - [XmlAttribute(RedmineKeys.PROPERTY)] - public string Property { get; set; } - - /// - /// Gets or sets the name. - /// - /// - /// The name. - /// - [XmlAttribute(RedmineKeys.NAME)] - public string Name { get; set; } - - /// - /// Gets or sets the old value. - /// - /// - /// The old value. - /// - [XmlElement(RedmineKeys.OLD_VALUE)] - public string OldValue { get; set; } - - /// - /// Gets or sets the new value. - /// - /// - /// The new value. - /// - [XmlElement(RedmineKeys.NEW_VALUE)] - public string NewValue { get; set; } - - /// - /// - /// - /// - public XmlSchema GetSchema() { return null; } - - /// - /// - /// - /// - public void ReadXml(XmlReader reader) - { - Property = reader.GetAttribute(RedmineKeys.PROPERTY); - Name = reader.GetAttribute(RedmineKeys.NAME); - - reader.Read(); - - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case RedmineKeys.OLD_VALUE: OldValue = reader.ReadElementContentAsString(); break; - - case RedmineKeys.NEW_VALUE: NewValue = reader.ReadElementContentAsString(); break; - - default: reader.Read(); break; - } - } - } - - /// - /// - /// - /// - public void WriteXml(XmlWriter writer) { } - - /// - /// - /// - /// - /// - public bool Equals(Detail other) - { - if (other == null) return false; - return (Property != null ? Property.Equals(other.Property) : other.Property == null) - && (Name != null ? Name.Equals(other.Name) : other.Name == null) - && (OldValue != null ? OldValue.Equals(other.OldValue) : other.OldValue == null) - && (NewValue != null ? NewValue.Equals(other.NewValue) : other.NewValue == null); - } - - /// - /// - /// - /// - /// - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals(obj as Detail); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Property, hashCode); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); - hashCode = HashCodeHelper.GetHashCode(OldValue, hashCode); - hashCode = HashCodeHelper.GetHashCode(NewValue, hashCode); - - return hashCode; - } - } - - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[Detail: Property={0}, Name={1}, OldValue={2}, NewValue={3}]", Property, Name, OldValue, NewValue); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/File.cs b/redmine-net20-api/Types/File.cs deleted file mode 100644 index a6cfde2a..00000000 --- a/redmine-net20-api/Types/File.cs +++ /dev/null @@ -1,163 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - - - -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Internals; -using System; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; - -namespace Redmine.Net.Api.Types -{ - [XmlRoot(RedmineKeys.FILE)] - public class File : Identifiable, IEquatable, IXmlSerializable - { - - [XmlElement(RedmineKeys.FILENAME)] - public string Filename { get; set; } - - [XmlElement(RedmineKeys.FILESIZE)] - public int Filesize { get; set; } - - [XmlElement(RedmineKeys.CONTENT_TYPE)] - public string ContentType { get; set; } - - [XmlElement(RedmineKeys.DESCRIPTION)] - public string Description { get; set; } - - [XmlElement(RedmineKeys.CONTENT_URL)] - public string ContentUrl { get; set; } - - [XmlElement(RedmineKeys.AUTHOR)] - public IdentifiableName Author { get; set; } - - [XmlElement(RedmineKeys.CREATED_ON)] - public DateTime? CreatedOn { get; set; } - - [XmlElement(RedmineKeys.VERSION)] - public IdentifiableName Version { get; set; } - - [XmlElement(RedmineKeys.DIGEST)] - public string Digest { get; set; } - - [XmlElement(RedmineKeys.DOWNLOADS)] - public int Downloads { get; set; } - - [XmlElement(RedmineKeys.TOKEN)] - public string Token { get; set; } - - public bool Equals(File other) - { - if (other == null) return false; - return (Id == other.Id - && Filename == other.Filename - && Filesize == other.Filesize - && Description == other.Description - && ContentType == other.ContentType - && ContentUrl == other.ContentUrl - && Author ==other.Author - && CreatedOn == other.CreatedOn - && Version == other.Version - && Digest == other.Digest - && Downloads == other.Downloads - && Token == other.Token - ); - } - - public override int GetHashCode() - { - var hashCode = base.GetHashCode(); - - hashCode = HashCodeHelper.GetHashCode(Filename, hashCode); - hashCode = HashCodeHelper.GetHashCode(Filesize, hashCode); - hashCode = HashCodeHelper.GetHashCode(ContentType, hashCode); - hashCode = HashCodeHelper.GetHashCode(Description, hashCode); - hashCode = HashCodeHelper.GetHashCode(Author, hashCode); - hashCode = HashCodeHelper.GetHashCode(ContentUrl, hashCode); - - hashCode = HashCodeHelper.GetHashCode(Author, hashCode); - hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(Version, hashCode); - hashCode = HashCodeHelper.GetHashCode(Digest, hashCode); - hashCode = HashCodeHelper.GetHashCode(Downloads, hashCode); - - return hashCode; - } - - public override string ToString() - { - return string.Format("[File: Id={0}, Name={1}]", Id, Filename); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals(obj as File); - } - - public XmlSchema GetSchema() - { - return null; - } - - public void ReadXml(XmlReader reader) - { - reader.Read(); - - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - case RedmineKeys.FILENAME: Filename = reader.ReadElementContentAsString(); break; - case RedmineKeys.FILESIZE: Filesize = reader.ReadElementContentAsInt(); break; - case RedmineKeys.CONTENT_TYPE: ContentType = reader.ReadElementContentAsString(); break; - case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; - case RedmineKeys.CONTENT_URL: ContentUrl = reader.ReadElementContentAsString(); break; - case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; - case RedmineKeys.CREATED_ON:CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; - case RedmineKeys.VERSION: Version = new IdentifiableName(reader); break; - case RedmineKeys.VERSION_ID: Version = new IdentifiableName() {Id = reader.ReadElementContentAsInt()}; break; - case RedmineKeys.DIGEST: Digest = reader.ReadElementContentAsString(); break; - case RedmineKeys.DOWNLOADS: Downloads = reader.ReadElementContentAsInt(); break; - case RedmineKeys.TOKEN:Token = reader.ReadElementContentAsString(); break; - default: - reader.Read(); - break; - } - } - } - - public void WriteXml(XmlWriter writer) - { - writer.WriteElementString(RedmineKeys.TOKEN, Token); - writer.WriteIdIfNotNull(Version, RedmineKeys.VERSION_ID); - writer.WriteElementString(RedmineKeys.FILENAME, Filename); - writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/Group.cs b/redmine-net20-api/Types/Group.cs deleted file mode 100755 index 3f9afca1..00000000 --- a/redmine-net20-api/Types/Group.cs +++ /dev/null @@ -1,172 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Xml; -using System.Xml.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 2.1 - /// - [XmlRoot(RedmineKeys.GROUP)] - public class Group : IdentifiableName, IEquatable - { - /// - /// Represents the group's users. - /// - [XmlArray(RedmineKeys.USERS)] - [XmlArrayItem(RedmineKeys.USER)] - public List Users { get; set; } - - /// - /// Gets or sets the custom fields. - /// - /// The custom fields. - [XmlArray(RedmineKeys.CUSTOM_FIELDS)] - [XmlArrayItem(RedmineKeys.CUSTOM_FIELD)] - public IList CustomFields { get; set; } - - /// - /// Gets or sets the custom fields. - /// - /// The custom fields. - [XmlArray(RedmineKeys.MEMBERSHIPS)] - [XmlArrayItem(RedmineKeys.MEMBERSHIP)] - public IList Memberships { get; set; } - - #region Implementation of IXmlSerializable - - /// - /// Generates an object from its XML representation. - /// - /// The stream from which the object is deserialized. - public override void ReadXml(XmlReader reader) - { - reader.Read(); - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; - - case RedmineKeys.USERS: Users = reader.ReadElementContentAsCollection(); break; - - case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadElementContentAsCollection(); break; - - case RedmineKeys.MEMBERSHIPS: Memberships = reader.ReadElementContentAsCollection(); break; - - default: reader.Read(); break; - } - } - } - - /// - /// Converts an object into its XML representation. - /// - /// The stream to which the object is serialized. - public override void WriteXml(XmlWriter writer) - { - writer.WriteElementString(RedmineKeys.NAME, Name); - writer.WriteArrayIds(Users, RedmineKeys.USER_IDS, typeof(int), GetGroupUserId); - } - - #endregion - - #region Implementation of IEquatable - - /// - /// Indicates whether the current object is equal to another object of the same type. - /// - /// - /// true if the current object is equal to the parameter; otherwise, false. - /// - /// An object to compare with this object. - public bool Equals(Group other) - { - if (other == null) return false; - return Id == other.Id - && Name == other.Name - && (Users != null ? Users.Equals(other.Users) : other.Users == null) - && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null) - && (Memberships != null ? Memberships.Equals(other.Memberships) : other.Memberships == null); - } - - /// - /// - /// - /// - /// - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals(obj as Group); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); - hashCode = HashCodeHelper.GetHashCode(Users, hashCode); - hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); - hashCode = HashCodeHelper.GetHashCode(Memberships, hashCode); - return hashCode; - } - } - - #endregion - - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[Group: Id={0}, Name={1}, Users={2}, CustomFields={3}, Memberships={4}]", Id, Name, Users, CustomFields, Memberships); - } - - /// - /// - /// - /// - /// - public int GetGroupUserId(object gu) - { - return ((GroupUser)gu).Id; - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/IRedmineManager.cs b/redmine-net20-api/Types/IRedmineManager.cs deleted file mode 100644 index e86a77b3..00000000 --- a/redmine-net20-api/Types/IRedmineManager.cs +++ /dev/null @@ -1,51 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System.Collections.Generic; -using System.Collections.Specialized; - -namespace Redmine.Net.Api.Types -{ - interface IRedmineManager - { - int PageSize { get; set; } - string ImpersonateUser { get; set; } - - User GetCurrentUser(NameValueCollection parameters = null); - - void AddUserToGroup(int groupId, int userId); - void RemoveUserFromGroup(int groupId, int userId); - void AddWatcherToIssue(int issueId, int userId); - void RemoveWatcherFromIssue(int issueId, int userId); - WikiPage GetWikiPage(string projectId, NameValueCollection parameters, string pageName, uint version = 0); - IList GetAllWikiPages(string projectId); - WikiPage CreateOrUpdateWikiPage(string projectId, string pageName, WikiPage wikiPage); - void DeleteWikiPage(string projectId, string pageName); - Upload UploadFile(byte[] data); - byte[] DownloadFile(string address); - List GetObjectList(NameValueCollection parameters) where T : class, new(); - List GetObjectList(NameValueCollection parameters, out int totalCount) where T : class, new(); - List GetTotalObjectList(NameValueCollection parameters) where T : class, new(); - List GetObjects (NameValueCollection parameters) where T: class, new(); - PaginatedObjects GetPaginatedObjects (NameValueCollection parameters)where T : class, new(); - T GetObject(string id, NameValueCollection parameters) where T : class, new(); - - T CreateObject(T obj, string ownerId = null) where T : class, new(); - void UpdateObject(string id, T obj) where T : class, new(); - void UpdateObject(string id, T obj, string projectId) where T : class, new(); - void DeleteObject(string id, NameValueCollection parameters) where T : class, new(); - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/IRedmineWebClient.cs b/redmine-net20-api/Types/IRedmineWebClient.cs deleted file mode 100644 index cda79526..00000000 --- a/redmine-net20-api/Types/IRedmineWebClient.cs +++ /dev/null @@ -1,45 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Specialized; -using System.Net; -using System.Net.Cache; - -namespace Redmine.Net.Api.Types -{ - interface IRedmineWebClient{ - Uri BaseAddress { get; set; } - NameValueCollection QueryString { get; set; } - - bool UseDefaultCredentials { get; set; } - ICredentials Credentials { get; set; } - - bool UseProxy { get; set; } - IWebProxy Proxy { get; set; } - - TimeSpan Timeout { get; set; } - - bool UseCookies { get; set; } - CookieContainer CookieContainer { get; set; } - - bool PreAuthenticate { get; set; } - - RequestCachePolicy CachePolicy { get; set; } - - bool KeepAlive { get; set; } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/IdentifiableName.cs b/redmine-net20-api/Types/IdentifiableName.cs deleted file mode 100755 index a45158b3..00000000 --- a/redmine-net20-api/Types/IdentifiableName.cs +++ /dev/null @@ -1,119 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Globalization; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// - /// - public class IdentifiableName : Identifiable, IXmlSerializable, IEquatable - { - /// - /// Initializes a new instance of the class. - /// - public IdentifiableName() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The reader. - public IdentifiableName(XmlReader reader) - { - Initialize(reader); - } - - private void Initialize(XmlReader reader) - { - ReadXml(reader); - } - - /// - /// Gets or sets the name. - /// - /// The name. - [XmlAttribute(RedmineKeys.NAME)] - public String Name { get; set; } - - /// - /// - /// - /// - public XmlSchema GetSchema() { return null; } - - /// - /// - /// - /// - public virtual void ReadXml(XmlReader reader) - { - Id = Convert.ToInt32(reader.GetAttribute(RedmineKeys.ID)); - Name = reader.GetAttribute(RedmineKeys.NAME); - reader.Read(); - } - - /// - /// - /// - /// - public virtual void WriteXml(XmlWriter writer) - { - writer.WriteAttributeString(RedmineKeys.ID, Id.ToString(CultureInfo.InvariantCulture)); - writer.WriteAttributeString(RedmineKeys.NAME, Name); - } - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[IdentifiableName: Id={0}, Name={1}]", Id, Name); - } - - /// - /// - /// - /// - /// - public bool Equals(IdentifiableName other) - { - if (other == null) return false; - return (Id == other.Id && Name == other.Name); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); - return hashCode; - } - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/Issue.cs b/redmine-net20-api/Types/Issue.cs deleted file mode 100644 index b81b7337..00000000 --- a/redmine-net20-api/Types/Issue.cs +++ /dev/null @@ -1,619 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu, Dorin Huzum. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// Available as of 1.1 : - ///include: fetch associated data (optional). - ///Possible values: children, attachments, relations, changesets and journals. To fetch multiple associations use comma (e.g ?include=relations,journals). - /// See Issue journals for more information. - /// - [XmlRoot(RedmineKeys.ISSUE)] - public class Issue : Identifiable, IXmlSerializable, IEquatable, ICloneable - { - /// - /// Gets or sets the project. - /// - /// The project. - [XmlElement(RedmineKeys.PROJECT)] - public IdentifiableName Project { get; set; } - - /// - /// Gets or sets the tracker. - /// - /// The tracker. - [XmlElement(RedmineKeys.TRACKER)] - public IdentifiableName Tracker { get; set; } - - /// - /// Gets or sets the status.Possible values: open, closed, * to get open and closed issues, status id - /// - /// The status. - [XmlElement(RedmineKeys.STATUS)] - public IdentifiableName Status { get; set; } - - /// - /// Gets or sets the priority. - /// - /// The priority. - [XmlElement(RedmineKeys.PRIORITY)] - public IdentifiableName Priority { get; set; } - - /// - /// Gets or sets the author. - /// - /// The author. - [XmlElement(RedmineKeys.AUTHOR)] - public IdentifiableName Author { get; set; } - - /// - /// Gets or sets the category. - /// - /// The category. - [XmlElement(RedmineKeys.CATEGORY)] - public IdentifiableName Category { get; set; } - - /// - /// Gets or sets the subject. - /// - /// The subject. - [XmlElement(RedmineKeys.SUBJECT)] - public String Subject { get; set; } - - /// - /// Gets or sets the description. - /// - /// The description. - [XmlElement(RedmineKeys.DESCRIPTION)] - public String Description { get; set; } - - /// - /// Gets or sets the start date. - /// - /// The start date. - [XmlElement(RedmineKeys.START_DATE, IsNullable = true)] - public DateTime? StartDate { get; set; } - - /// - /// Gets or sets the due date. - /// - /// The due date. - [XmlElement(RedmineKeys.DUE_DATE, IsNullable = true)] - public DateTime? DueDate { get; set; } - - /// - /// Gets or sets the done ratio. - /// - /// The done ratio. - [XmlElement(RedmineKeys.DONE_RATIO, IsNullable = true)] - public float? DoneRatio { get; set; } - - /// - /// Gets or sets a value indicating whether [private notes]. - /// - /// - /// true if [private notes]; otherwise, false. - /// - [XmlElement(RedmineKeys.PRIVATE_NOTES)] - public bool PrivateNotes { get; set; } - - /// - /// Gets or sets the estimated hours. - /// - /// The estimated hours. - [XmlElement(RedmineKeys.ESTIMATED_HOURS, IsNullable = true)] - public float? EstimatedHours { get; set; } - - /// - /// Gets or sets the hours spent on the issue. - /// - /// The hours spent on the issue. - [XmlElement(RedmineKeys.SPENT_HOURS, IsNullable = true)] - public float? SpentHours { get; set; } - - /// - /// Gets or sets the custom fields. - /// - /// The custom fields. - [XmlArray(RedmineKeys.CUSTOM_FIELDS)] - [XmlArrayItem(RedmineKeys.CUSTOM_FIELD)] - public IList CustomFields { get; set; } - - /// - /// Gets or sets the created on. - /// - /// The created on. - [XmlElement(RedmineKeys.CREATED_ON, IsNullable = true)] - public DateTime? CreatedOn { get; set; } - - /// - /// Gets or sets the updated on. - /// - /// The updated on. - [XmlElement(RedmineKeys.UPDATED_ON, IsNullable = true)] - public DateTime? UpdatedOn { get; set; } - - /// - /// Gets or sets the closed on. - /// - /// The closed on. - [XmlElement(RedmineKeys.CLOSED_ON, IsNullable = true)] - public DateTime? ClosedOn { get; set; } - - /// - /// Gets or sets the notes. - /// - [XmlElement(RedmineKeys.NOTES)] - public string Notes { get; set; } - - /// - /// Gets or sets the ID of the user to assign the issue to (currently no mechanism to assign by name). - /// - /// - /// The assigned to. - /// - [XmlElement(RedmineKeys.ASSIGNED_TO)] - public IdentifiableName AssignedTo { get; set; } - - /// - /// Gets or sets the parent issue id. Only when a new issue is created this property shall be used. - /// - /// - /// The parent issue id. - /// - [XmlElement(RedmineKeys.PARENT)] - public IdentifiableName ParentIssue { get; set; } - - /// - /// Gets or sets the fixed version. - /// - /// - /// The fixed version. - /// - [XmlElement(RedmineKeys.FIXED_VERSION)] - public IdentifiableName FixedVersion { get; set; } - - /// - /// indicate whether the issue is private or not - /// - /// - /// true if this issue is private; otherwise, false. - /// - [XmlElement(RedmineKeys.IS_PRIVATE)] - public bool IsPrivate { get; set; } - - /// - /// Returns the sum of spent hours of the task and all the subtasks. - /// - /// Availability starting with redmine version 3.3 - [XmlElement(RedmineKeys.TOTAL_SPENT_HOURS)] - public float? TotalSpentHours { get; set; } - - /// - /// Returns the sum of estimated hours of task and all the subtasks. - /// - /// Availability starting with redmine version 3.3 - [XmlElement(RedmineKeys.TOTAL_ESTIMATED_HOURS)] - public float? TotalEstimatedHours { get; set; } - - /// - /// Gets or sets the journals. - /// - /// - /// The journals. - /// - [XmlArray(RedmineKeys.JOURNALS)] - [XmlArrayItem(RedmineKeys.JOURNAL)] - public IList Journals { get; set; } - - /// - /// Gets or sets the changesets. - /// - /// - /// The changesets. - /// - [XmlArray(RedmineKeys.CHANGESETS)] - [XmlArrayItem(RedmineKeys.CHANGESET)] - public IList Changesets { get; set; } - - /// - /// Gets or sets the attachments. - /// - /// - /// The attachments. - /// - [XmlArray(RedmineKeys.ATTACHMENTS)] - [XmlArrayItem(RedmineKeys.ATTACHMENT)] - public IList Attachments { get; set; } - - /// - /// Gets or sets the issue relations. - /// - /// - /// The issue relations. - /// - [XmlArray(RedmineKeys.RELATIONS)] - [XmlArrayItem(RedmineKeys.RELATION)] - public IList Relations { get; set; } - - /// - /// Gets or sets the issue children. - /// - /// - /// The issue children. - /// NOTE: Only Id, tracker and subject are filled. - /// - [XmlArray(RedmineKeys.CHILDREN)] - [XmlArrayItem(RedmineKeys.ISSUE)] - public IList Children { get; set; } - - /// - /// Gets or sets the attachments. - /// - /// - /// The attachment. - /// - [XmlArray(RedmineKeys.UPLOADS)] - [XmlArrayItem(RedmineKeys.UPLOAD)] - public IList Uploads { get; set; } - - /// - /// - /// - [XmlArray(RedmineKeys.WATCHERS)] - [XmlArrayItem(RedmineKeys.WATCHER)] - public IList Watchers { get; set; } - - /// - /// - /// - /// - public XmlSchema GetSchema() - { - return null; - } - - /// - /// - /// - /// - public void ReadXml(XmlReader reader) - { - reader.Read(); - - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case RedmineKeys.ID: - Id = reader.ReadElementContentAsInt(); - break; - - case RedmineKeys.PROJECT: - Project = new IdentifiableName(reader); - break; - - case RedmineKeys.TRACKER: - Tracker = new IdentifiableName(reader); - break; - - case RedmineKeys.STATUS: - Status = new IdentifiableName(reader); - break; - - case RedmineKeys.PRIORITY: - Priority = new IdentifiableName(reader); - break; - - case RedmineKeys.AUTHOR: - Author = new IdentifiableName(reader); - break; - - case RedmineKeys.ASSIGNED_TO: - AssignedTo = new IdentifiableName(reader); - break; - - case RedmineKeys.CATEGORY: - Category = new IdentifiableName(reader); - break; - - case RedmineKeys.PARENT: - ParentIssue = new IdentifiableName(reader); - break; - - case RedmineKeys.FIXED_VERSION: - FixedVersion = new IdentifiableName(reader); - break; - - case RedmineKeys.PRIVATE_NOTES: - PrivateNotes = reader.ReadElementContentAsBoolean(); - break; - - case RedmineKeys.IS_PRIVATE: - IsPrivate = reader.ReadElementContentAsBoolean(); - break; - - case RedmineKeys.SUBJECT: - Subject = reader.ReadElementContentAsString(); - break; - - case RedmineKeys.NOTES: - Notes = reader.ReadElementContentAsString(); - break; - - case RedmineKeys.DESCRIPTION: - Description = reader.ReadElementContentAsString(); - break; - - case RedmineKeys.START_DATE: - StartDate = reader.ReadElementContentAsNullableDateTime(); - break; - - case RedmineKeys.DUE_DATE: - DueDate = reader.ReadElementContentAsNullableDateTime(); - break; - - case RedmineKeys.DONE_RATIO: - DoneRatio = reader.ReadElementContentAsNullableFloat(); - break; - - case RedmineKeys.ESTIMATED_HOURS: - EstimatedHours = reader.ReadElementContentAsNullableFloat(); - break; - - case RedmineKeys.TOTAL_ESTIMATED_HOURS: - TotalEstimatedHours = reader.ReadElementContentAsNullableFloat(); - break; - - case RedmineKeys.TOTAL_SPENT_HOURS: - TotalSpentHours = reader.ReadElementContentAsNullableFloat(); - break; - - case RedmineKeys.SPENT_HOURS: - SpentHours = reader.ReadElementContentAsNullableFloat(); - break; - - case RedmineKeys.CREATED_ON: - CreatedOn = reader.ReadElementContentAsNullableDateTime(); - break; - - case RedmineKeys.UPDATED_ON: - UpdatedOn = reader.ReadElementContentAsNullableDateTime(); - break; - - case RedmineKeys.CLOSED_ON: - ClosedOn = reader.ReadElementContentAsNullableDateTime(); - break; - - case RedmineKeys.CUSTOM_FIELDS: - CustomFields = reader.ReadElementContentAsCollection(); - break; - - case RedmineKeys.ATTACHMENTS: - Attachments = reader.ReadElementContentAsCollection(); - break; - - case RedmineKeys.RELATIONS: - Relations = reader.ReadElementContentAsCollection(); - break; - - case RedmineKeys.JOURNALS: - Journals = reader.ReadElementContentAsCollection(); - break; - - case RedmineKeys.CHANGESETS: - Changesets = reader.ReadElementContentAsCollection(); - break; - - case RedmineKeys.CHILDREN: - Children = reader.ReadElementContentAsCollection(); - break; - - case RedmineKeys.WATCHERS: - Watchers = reader.ReadElementContentAsCollection(); - break; - - default: - reader.Read(); - break; - } - } - } - - /// - /// - /// - /// - public void WriteXml(XmlWriter writer) - { - writer.WriteElementString(RedmineKeys.SUBJECT, Subject); - writer.WriteElementString(RedmineKeys.NOTES, Notes); - - if (Id != 0) - { - writer.WriteElementString(RedmineKeys.PRIVATE_NOTES, PrivateNotes.ToString().ToLowerInvariant()); - } - - writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); - writer.WriteStartElement(RedmineKeys.IS_PRIVATE); - writer.WriteValue(IsPrivate.ToString().ToLowerInvariant()); - writer.WriteEndElement(); - - writer.WriteIdIfNotNull(Project, RedmineKeys.PROJECT_ID); - writer.WriteIdIfNotNull(Priority, RedmineKeys.PRIORITY_ID); - writer.WriteIdIfNotNull(Status, RedmineKeys.STATUS_ID); - writer.WriteIdIfNotNull(Category, RedmineKeys.CATEGORY_ID); - writer.WriteIdIfNotNull(Tracker, RedmineKeys.TRACKER_ID); - writer.WriteIdIfNotNull(AssignedTo, RedmineKeys.ASSIGNED_TO_ID); - writer.WriteIdIfNotNull(ParentIssue, RedmineKeys.PARENT_ISSUE_ID); - writer.WriteIdIfNotNull(FixedVersion, RedmineKeys.FIXED_VERSION_ID); - - writer.WriteValueOrEmpty(EstimatedHours, RedmineKeys.ESTIMATED_HOURS); - writer.WriteIfNotDefaultOrNull(DoneRatio, RedmineKeys.DONE_RATIO); - writer.WriteDateOrEmpty(StartDate, RedmineKeys.START_DATE); - writer.WriteDateOrEmpty(DueDate, RedmineKeys.DUE_DATE); - writer.WriteDateOrEmpty(UpdatedOn, RedmineKeys.UPDATED_ON); - - writer.WriteArray(Uploads, RedmineKeys.UPLOADS); - writer.WriteArray(CustomFields, RedmineKeys.CUSTOM_FIELDS); - - writer.WriteListElements(Watchers as IList, RedmineKeys.WATCHER_USER_IDS); - } - - /// - /// - /// - /// - public object Clone() - { - var issue = new Issue - { - AssignedTo = AssignedTo, - Author = Author, - Category = Category, - CustomFields = CustomFields.Clone(), - Description = Description, - DoneRatio = DoneRatio, - DueDate = DueDate, - SpentHours = SpentHours, - EstimatedHours = EstimatedHours, - Priority = Priority, - StartDate = StartDate, - Status = Status, - Subject = Subject, - Tracker = Tracker, - Project = Project, - FixedVersion = FixedVersion, - Notes = Notes, - Watchers = Watchers.Clone() - }; - return issue; - } - - /// - /// - /// - /// - /// - public bool Equals(Issue other) - { - if (other == null) return false; - return ( - Id == other.Id - && Project == other.Project - && Tracker == other.Tracker - && Status == other.Status - && Priority == other.Priority - && Author == other.Author - && Category == other.Category - && Subject == other.Subject - && Description == other.Description - && StartDate == other.StartDate - && DueDate == other.DueDate - && DoneRatio == other.DoneRatio - && EstimatedHours == other.EstimatedHours - && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null) - && CreatedOn == other.CreatedOn - && UpdatedOn == other.UpdatedOn - && AssignedTo == other.AssignedTo - && FixedVersion == other.FixedVersion - && Notes == other.Notes - && (Watchers != null ? Watchers.Equals(other.Watchers) : other.Watchers == null) - && ClosedOn == other.ClosedOn - && SpentHours == other.SpentHours - && PrivateNotes == other.PrivateNotes - && (Attachments != null ? Attachments.Equals(other.Attachments) : other.Attachments == null) - && (Changesets!= null ? Changesets.Equals(other.Changesets) : other.Changesets == null) - && (Children != null ? Children.Equals(other.Children) : other.Children == null) - && (Journals != null ? Journals.Equals(other.Journals) : other.Journals == null) - && (Relations != null ? Relations.Equals(other.Relations) : other.Relations == null) - ); - } - - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[Issue: {30}, Project={0}, Tracker={1}, Status={2}, Priority={3}, Author={4}, Category={5}, Subject={6}, Description={7}, StartDate={8}, DueDate={9}, DoneRatio={10}, PrivateNotes={11}, EstimatedHours={12}, SpentHours={13}, CustomFields={14}, CreatedOn={15}, UpdatedOn={16}, ClosedOn={17}, Notes={18}, AssignedTo={19}, ParentIssue={20}, FixedVersion={21}, IsPrivate={22}, Journals={23}, Changesets={24}, Attachments={25}, Relations={26}, Children={27}, Uploads={28}, Watchers={29}]", - Project, Tracker, Status, Priority, Author, Category, Subject, Description, StartDate, DueDate, DoneRatio, PrivateNotes, - EstimatedHours, SpentHours, CustomFields, CreatedOn, UpdatedOn, ClosedOn, Notes, AssignedTo, ParentIssue, FixedVersion, - IsPrivate, Journals, Changesets, Attachments, Relations, Children, Uploads, Watchers, base.ToString()); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - var hashCode = base.GetHashCode(); - - hashCode = HashCodeHelper.GetHashCode(Project, hashCode); - hashCode = HashCodeHelper.GetHashCode(Tracker, hashCode); - hashCode = HashCodeHelper.GetHashCode(Status, hashCode); - hashCode = HashCodeHelper.GetHashCode(Priority, hashCode); - hashCode = HashCodeHelper.GetHashCode(Author, hashCode); - hashCode = HashCodeHelper.GetHashCode(Category, hashCode); - - hashCode = HashCodeHelper.GetHashCode(Subject, hashCode); - hashCode = HashCodeHelper.GetHashCode(Description, hashCode); - hashCode = HashCodeHelper.GetHashCode(StartDate, hashCode); - hashCode = HashCodeHelper.GetHashCode(Project, hashCode); - hashCode = HashCodeHelper.GetHashCode(DueDate, hashCode); - hashCode = HashCodeHelper.GetHashCode(DoneRatio, hashCode); - - hashCode = HashCodeHelper.GetHashCode(PrivateNotes, hashCode); - hashCode = HashCodeHelper.GetHashCode(EstimatedHours, hashCode); - hashCode = HashCodeHelper.GetHashCode(SpentHours, hashCode); - hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); - - hashCode = HashCodeHelper.GetHashCode(Notes, hashCode); - hashCode = HashCodeHelper.GetHashCode(AssignedTo, hashCode); - hashCode = HashCodeHelper.GetHashCode(ParentIssue, hashCode); - hashCode = HashCodeHelper.GetHashCode(FixedVersion, hashCode); - hashCode = HashCodeHelper.GetHashCode(IsPrivate, hashCode); - hashCode = HashCodeHelper.GetHashCode(Journals, hashCode); - hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); - - hashCode = HashCodeHelper.GetHashCode(Changesets, hashCode); - hashCode = HashCodeHelper.GetHashCode(Attachments, hashCode); - hashCode = HashCodeHelper.GetHashCode(Relations, hashCode); - hashCode = HashCodeHelper.GetHashCode(Children, hashCode); - hashCode = HashCodeHelper.GetHashCode(Uploads, hashCode); - hashCode = HashCodeHelper.GetHashCode(Watchers, hashCode); - - return hashCode; - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/IssueCategory.cs b/redmine-net20-api/Types/IssueCategory.cs deleted file mode 100755 index ed172866..00000000 --- a/redmine-net20-api/Types/IssueCategory.cs +++ /dev/null @@ -1,143 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 1.3 - /// - [XmlRoot(RedmineKeys.ISSUE_CATEGORY)] - public class IssueCategory : Identifiable, IEquatable, IXmlSerializable - { - /// - /// Gets or sets the project. - /// - /// - /// The project. - /// - [XmlElement(RedmineKeys.PROJECT)] - public IdentifiableName Project { get; set; } - - /// - /// Gets or sets the asign to. - /// - /// - /// The asign to. - /// - [XmlElement(RedmineKeys.ASSIGNED_TO)] - public IdentifiableName AsignTo { get; set; } - - /// - /// Gets or sets the name. - /// - /// - /// The name. - /// - [XmlElement(RedmineKeys.NAME)] - public string Name { get; set; } - - /// - /// - /// - /// - /// - public bool Equals(IssueCategory other) - { - if (other == null) return false; - return (Id == other.Id && Project == other.Project && AsignTo == other.AsignTo && Name == other.Name); - } - - /// - /// - /// - /// - public XmlSchema GetSchema() { return null; } - - /// - /// - /// - /// - public void ReadXml(XmlReader reader) - { - reader.Read(); - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; - - case RedmineKeys.ASSIGNED_TO: AsignTo = new IdentifiableName(reader); break; - - case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; - - default: reader.Read(); break; - } - } - } - - /// - /// - /// - /// - public void WriteXml(XmlWriter writer) - { - writer.WriteIdIfNotNull(Project, RedmineKeys.PROJECT_ID); - writer.WriteElementString(RedmineKeys.NAME, Name); - writer.WriteIdIfNotNull(AsignTo, RedmineKeys.ASSIGNED_TO_ID); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Project, hashCode); - hashCode = HashCodeHelper.GetHashCode(AsignTo, hashCode); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); - return hashCode; - } - } - - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[IssueCategory: {3}, Project={0}, AsignTo={1}, Name={2}]", Project, AsignTo, Name, base.ToString()); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/IssueChild.cs b/redmine-net20-api/Types/IssueChild.cs deleted file mode 100755 index eee5ddda..00000000 --- a/redmine-net20-api/Types/IssueChild.cs +++ /dev/null @@ -1,131 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// - /// - [XmlRoot(RedmineKeys.ISSUE)] - public class IssueChild : Identifiable, IXmlSerializable, IEquatable, ICloneable - { - /// - /// Gets or sets the tracker. - /// - /// The tracker. - [XmlElement(RedmineKeys.TRACKER)] - public IdentifiableName Tracker { get; set; } - - /// - /// Gets or sets the subject. - /// - /// The subject. - [XmlElement(RedmineKeys.SUBJECT)] - public String Subject { get; set; } - - /// - /// - /// - /// - public XmlSchema GetSchema() { return null; } - - /// - /// - /// - /// - public void ReadXml(XmlReader reader) - { - Id = Convert.ToInt32(reader.GetAttribute(RedmineKeys.ID)); - reader.Read(); - - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case RedmineKeys.TRACKER: Tracker = new IdentifiableName(reader); break; - - case RedmineKeys.SUBJECT: Subject = reader.ReadElementContentAsString(); break; - - default: reader.Read(); break; - } - } - } - - /// - /// - /// - /// - public void WriteXml(XmlWriter writer) { } - - /// - /// - /// - /// - public object Clone() - { - var issueChild = new IssueChild { Subject = Subject, Tracker = Tracker }; - return issueChild; - } - - /// - /// - /// - /// - /// - public bool Equals(IssueChild other) - { - if (other == null) return false; - return (Id == other.Id && Tracker == other.Tracker && Subject == other.Subject); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Tracker, hashCode); - hashCode = HashCodeHelper.GetHashCode(Subject, hashCode); - return hashCode; - } - } - - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[IssueChild: {0}, Tracker={1}, Subject={2}]", base.ToString(), Tracker, Subject); - } - } -} diff --git a/redmine-net20-api/Types/IssueCustomField.cs b/redmine-net20-api/Types/IssueCustomField.cs deleted file mode 100755 index c1336673..00000000 --- a/redmine-net20-api/Types/IssueCustomField.cs +++ /dev/null @@ -1,147 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Xml; -using System.Xml.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// - /// - [XmlRoot(RedmineKeys.CUSTOM_FIELD)] - public class IssueCustomField : IdentifiableName, IEquatable, ICloneable - { - /// - /// Gets or sets the value. - /// - /// The value. - [XmlArray(RedmineKeys.VALUE)] - [XmlArrayItem(RedmineKeys.VALUE)] - public IList Values { get; set; } - - /// - /// - /// - [XmlAttribute(RedmineKeys.MULTIPLE)] - public bool Multiple { get; set; } - - /// - /// - /// - /// - public override void ReadXml(XmlReader reader) - { - Id = Convert.ToInt32(reader.GetAttribute(RedmineKeys.ID)); - Name = reader.GetAttribute(RedmineKeys.NAME); - - Multiple = reader.ReadAttributeAsBoolean(RedmineKeys.MULTIPLE); - reader.Read(); - - if (string.IsNullOrEmpty(reader.GetAttribute("type"))) - { - Values = new List { new CustomFieldValue { Info = reader.ReadElementContentAsString() } }; - } - else - { - var result = reader.ReadElementContentAsCollection(); - Values = result; - } - } - - /// - /// - /// - /// - public override void WriteXml(XmlWriter writer) - { - if (Values == null) return; - var itemsCount = Values.Count; - - writer.WriteAttributeString(RedmineKeys.ID, Id.ToString(CultureInfo.InvariantCulture)); - if (itemsCount > 1) - { - writer.WriteArrayStringElement(Values, RedmineKeys.VALUE, GetValue); - } - else - { - writer.WriteElementString(RedmineKeys.VALUE, itemsCount > 0 ? Values[0].Info : null); - } - } - - /// - /// - /// - /// - /// - public bool Equals(IssueCustomField other) - { - if (other == null) return false; - return (Id == other.Id && Name == other.Name && Multiple == other.Multiple && Values.Equals(other.Values)); - } - - /// - /// - /// - /// - public object Clone() - { - var issueCustomField = new IssueCustomField { Multiple = Multiple, Values = Values.Clone() }; - return issueCustomField; - } - - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[IssueCustomField: {2} Values={0}, Multiple={1}]", Values, Multiple, base.ToString()); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); - hashCode = HashCodeHelper.GetHashCode(Values, hashCode); - hashCode = HashCodeHelper.GetHashCode(Multiple, hashCode); - return hashCode; - } - } - - /// - /// - /// - /// - /// - public string GetValue(object item) - { - return ((CustomFieldValue)item).Info; - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/IssueRelation.cs b/redmine-net20-api/Types/IssueRelation.cs deleted file mode 100755 index 9f67c8bb..00000000 --- a/redmine-net20-api/Types/IssueRelation.cs +++ /dev/null @@ -1,172 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 1.3 - /// - [XmlRoot(RedmineKeys.RELATION)] - public class IssueRelation : Identifiable, IXmlSerializable, IEquatable - { - /// - /// Gets or sets the issue id. - /// - /// The issue id. - [XmlElement(RedmineKeys.ISSUE_ID)] - public int IssueId { get; set; } - - /// - /// Gets or sets the related issue id. - /// - /// The issue to id. - [XmlElement(RedmineKeys.ISSUE_TO_ID)] - public int IssueToId { get; set; } - - /// - /// Gets or sets the type of relation. - /// - /// The type. - [XmlElement(RedmineKeys.RELATION_TYPE)] - public IssueRelationType Type { get; set; } - - /// - /// Gets or sets the delay for a "precedes" or "follows" relation. - /// - /// The delay. - [XmlElement(RedmineKeys.DELAY, IsNullable = true)] - public int? Delay { get; set; } - - /// - /// - /// - /// - public XmlSchema GetSchema() { return null; } - - /// - /// - /// - /// - public void ReadXml(XmlReader reader) - { - if (!reader.IsEmptyElement) reader.Read(); - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - if (reader.IsEmptyElement && reader.HasAttributes) - { - while (reader.MoveToNextAttribute()) - { - var attributeName = reader.Name; - switch (reader.Name) - { - case RedmineKeys.ID: Id = reader.ReadAttributeAsInt(attributeName); break; - case RedmineKeys.ISSUE_ID: IssueId = reader.ReadAttributeAsInt(attributeName); break; - case RedmineKeys.ISSUE_TO_ID: IssueToId = reader.ReadAttributeAsInt(attributeName); break; - case RedmineKeys.RELATION_TYPE: - var rt = reader.GetAttribute(attributeName); - if (!string.IsNullOrEmpty(rt)) - { - Type = (IssueRelationType)Enum.Parse(typeof(IssueRelationType), rt, true); - } - break; - case RedmineKeys.DELAY: Delay = reader.ReadAttributeAsNullableInt(attributeName); break; - } - } - return; - } - - switch (reader.Name) - { - case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - case RedmineKeys.ISSUE_ID: IssueId = reader.ReadElementContentAsInt(); break; - case RedmineKeys.ISSUE_TO_ID: IssueToId = reader.ReadElementContentAsInt(); break; - case RedmineKeys.RELATION_TYPE: - var rt = reader.ReadElementContentAsString(); - if (!string.IsNullOrEmpty(rt)) - { - Type = (IssueRelationType)Enum.Parse(typeof(IssueRelationType), rt, true); - } - break; - case RedmineKeys.DELAY: Delay = reader.ReadElementContentAsNullableInt(); break; - default: reader.Read(); break; - } - } - } - - /// - /// - /// - /// - public void WriteXml(XmlWriter writer) - { - writer.WriteElementString(RedmineKeys.ISSUE_TO_ID, IssueToId.ToString()); - writer.WriteElementString(RedmineKeys.RELATION_TYPE, Type.ToString()); - if (Type == IssueRelationType.precedes || Type == IssueRelationType.follows) - writer.WriteValueOrEmpty(Delay, RedmineKeys.DELAY); - } - - /// - /// - /// - /// - /// - public bool Equals(IssueRelation other) - { - if (other == null) return false; - return (Id == other.Id && IssueId == other.IssueId && IssueToId == other.IssueToId && Type == other.Type && Delay == other.Delay); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(IssueId, hashCode); - hashCode = HashCodeHelper.GetHashCode(IssueToId, hashCode); - hashCode = HashCodeHelper.GetHashCode(Type, hashCode); - hashCode = HashCodeHelper.GetHashCode(Delay, hashCode); - return hashCode; - } - } - - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[IssueRelation: {4}, IssueId={0}, IssueToId={1}, Type={2}, Delay={3}]", IssueId, IssueToId, Type, Delay, base.ToString()); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/IssueStatus.cs b/redmine-net20-api/Types/IssueStatus.cs deleted file mode 100755 index 30ce58d3..00000000 --- a/redmine-net20-api/Types/IssueStatus.cs +++ /dev/null @@ -1,119 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Xml; -using System.Xml.Serialization; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 1.3 - /// - [XmlRoot(RedmineKeys.ISSUE_STATUS)] - public class IssueStatus : IdentifiableName, IEquatable - { - /// - /// Gets or sets a value indicating whether IssueStatus is default. - /// - /// - /// true if IssueStatus is default; otherwise, false. - /// - [XmlElement(RedmineKeys.IS_DEFAULT)] - public bool IsDefault { get; set; } - - /// - /// Gets or sets a value indicating whether IssueStatus is closed. - /// - /// true if IssueStatus is closed; otherwise, false. - [XmlElement(RedmineKeys.IS_CLOSED)] - public bool IsClosed { get; set; } - - /// - /// - /// - /// - public override void ReadXml(XmlReader reader) - { - reader.Read(); - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; - - case RedmineKeys.IS_DEFAULT: IsDefault = reader.ReadElementContentAsBoolean(); break; - - case RedmineKeys.IS_CLOSED: IsClosed = reader.ReadElementContentAsBoolean(); break; - - default: reader.Read(); break; - } - } - } - - /// - /// - /// - /// - public override void WriteXml(XmlWriter writer) { } - - /// - /// - /// - /// - /// - public bool Equals(IssueStatus other) - { - if (other == null) return false; - return (Id == other.Id && Name == other.Name && IsClosed == other.IsClosed && IsDefault == other.IsDefault); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); - hashCode = HashCodeHelper.GetHashCode(IsClosed, hashCode); - hashCode = HashCodeHelper.GetHashCode(IsDefault, hashCode); - return hashCode; - } - } - - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[IssueStatus: {2}, IsDefault={0}, IsClosed={1}]", IsDefault, IsClosed, base.ToString()); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/Journal.cs b/redmine-net20-api/Types/Journal.cs deleted file mode 100755 index 59ab44cf..00000000 --- a/redmine-net20-api/Types/Journal.cs +++ /dev/null @@ -1,169 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// - /// - [XmlRoot(RedmineKeys.JOURNAL)] - public class Journal : Identifiable, IEquatable, IXmlSerializable - { - /// - /// Gets or sets the user. - /// - /// - /// The user. - /// - [XmlElement(RedmineKeys.USER)] - public IdentifiableName User { get; set; } - - /// - /// Gets or sets the notes. - /// - /// - /// The notes. - /// - [XmlElement(RedmineKeys.NOTES)] - public string Notes { get; set; } - - /// - /// Gets or sets the created on. - /// - /// - /// The created on. - /// - [XmlElement(RedmineKeys.CREATED_ON, IsNullable = true)] - public DateTime? CreatedOn { get; set; } - - /// - /// Gets or sets the details. - /// - /// - /// The details. - /// - [XmlArray(RedmineKeys.DETAILS)] - [XmlArrayItem(RedmineKeys.DETAIL)] - public IList Details { get; set; } - - /// - /// - /// - /// - public XmlSchema GetSchema() { return null; } - - /// - /// - /// - /// - public void ReadXml(XmlReader reader) - { - Id = reader.ReadAttributeAsInt(RedmineKeys.ID); - reader.Read(); - - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case RedmineKeys.USER: User = new IdentifiableName(reader); break; - - case RedmineKeys.NOTES: Notes = reader.ReadElementContentAsString(); break; - - case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case RedmineKeys.DETAILS: Details = reader.ReadElementContentAsCollection(); break; - - default: reader.Read(); break; - } - } - } - - /// - /// - /// - /// - public void WriteXml(XmlWriter writer) { } - - /// - /// - /// - /// - /// - public bool Equals(Journal other) - { - if (other == null) return false; - return Id == other.Id - && User == other.User - && Notes == other.Notes - && CreatedOn == other.CreatedOn - && (Details != null ? Details.Equals(other.Details) : other.Details == null ); - } - - /// - /// - /// - /// - /// - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals(obj as Journal); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(User, hashCode); - hashCode = HashCodeHelper.GetHashCode(Notes, hashCode); - hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(Details, hashCode); - return hashCode; - } - } - - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[Journal: Id={0}, User={1}, Notes={2}, CreatedOn={3}, Details={4}]", Id, User, Notes, CreatedOn, Details); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/Membership.cs b/redmine-net20-api/Types/Membership.cs deleted file mode 100755 index 1c1ca2c9..00000000 --- a/redmine-net20-api/Types/Membership.cs +++ /dev/null @@ -1,126 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// Only the roles can be updated, the project and the user of a membership are read-only. - /// - [XmlRoot(RedmineKeys.MEMBERSHIP)] - public class Membership : Identifiable, IEquatable, IXmlSerializable - { - /// - /// Gets or sets the project. - /// - /// The project. - [XmlElement(RedmineKeys.PROJECT)] - public IdentifiableName Project { get; set; } - - /// - /// Gets or sets the type. - /// - /// The type. - [XmlArray(RedmineKeys.ROLES)] - [XmlArrayItem(RedmineKeys.ROLE)] - public List Roles { get; set; } - - /// - /// - /// - /// - public XmlSchema GetSchema() { return null; } - - /// - /// - /// - /// - public void ReadXml(XmlReader reader) - { - reader.Read(); - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; - - case RedmineKeys.ROLES: Roles = reader.ReadElementContentAsCollection(); break; - - default: reader.Read(); break; - } - } - } - - /// - /// - /// - /// - public void WriteXml(XmlWriter writer) { } - - /// - /// - /// - /// - /// - public bool Equals(Membership other) - { - if (other == null) return false; - return (Id == other.Id && - (Project != null ? Project.Equals(other.Project) : other.Project == null) && - (Roles != null ? Roles.Equals(other.Roles) : other.Roles == null)); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Project, hashCode); - //hashCode = Utils.GetHashCode(Roles, hashCode); - return hashCode; - } - } - - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[Membership: {2}, Project={0}, Roles={1}]", Project, Roles, base.ToString()); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/MembershipRole.cs b/redmine-net20-api/Types/MembershipRole.cs deleted file mode 100755 index 24a06b56..00000000 --- a/redmine-net20-api/Types/MembershipRole.cs +++ /dev/null @@ -1,97 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Xml; -using System.Xml.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// - /// - [XmlRoot(RedmineKeys.ROLE)] - public class MembershipRole : IdentifiableName, IEquatable - { - /// - /// Gets or sets a value indicating whether this is inherited. - /// - /// - /// true if inherited; otherwise, false. - /// - [XmlAttribute(RedmineKeys.INHERITED)] - public bool Inherited { get; set; } - - /// - /// Reads the XML. - /// - /// The reader. - public override void ReadXml(XmlReader reader) - { - Id = Convert.ToInt32(reader.GetAttribute(RedmineKeys.ID)); - Name = reader.GetAttribute(RedmineKeys.NAME); - Inherited = reader.ReadAttributeAsBoolean(RedmineKeys.INHERITED); - reader.Read(); - } - - /// - /// - /// - /// - public override void WriteXml(XmlWriter writer) - { - writer.WriteValue(Id); - } - - /// - /// - /// - /// - /// - public bool Equals(MembershipRole other) - { - if (other == null) return false; - return Id == other.Id && Name == other.Name && Inherited == other.Inherited; - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); - hashCode = HashCodeHelper.GetHashCode(Inherited, hashCode); - return hashCode; - } - } - - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[MembershipRole: {1}, Inherited={0}]", Inherited, base.ToString()); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/News.cs b/redmine-net20-api/Types/News.cs deleted file mode 100755 index d5132491..00000000 --- a/redmine-net20-api/Types/News.cs +++ /dev/null @@ -1,168 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 1.1 - /// - [XmlRoot(RedmineKeys.NEWS)] - public class News : Identifiable, IEquatable, IXmlSerializable - { - /// - /// Gets or sets the project. - /// - /// The project. - [XmlElement(RedmineKeys.PROJECT)] - public IdentifiableName Project { get; set; } - - /// - /// Gets or sets the author. - /// - /// The author. - [XmlElement(RedmineKeys.AUTHOR)] - public IdentifiableName Author { get; set; } - - /// - /// Gets or sets the title. - /// - /// The title. - [XmlElement(RedmineKeys.TITLE)] - public String Title { get; set; } - - /// - /// Gets or sets the summary. - /// - /// The summary. - [XmlElement(RedmineKeys.SUMMARY)] - public String Summary { get; set; } - - /// - /// Gets or sets the description. - /// - /// The description. - [XmlElement(RedmineKeys.DESCRIPTION)] - public String Description { get; set; } - - /// - /// Gets or sets the created on. - /// - /// The created on. - [XmlElement(RedmineKeys.CREATED_ON, IsNullable = true)] - public DateTime? CreatedOn { get; set; } - - /// - /// - /// - /// - public XmlSchema GetSchema() { return null; } - - /// - /// - /// - /// - public void ReadXml(XmlReader reader) - { - reader.Read(); - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; - - case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; - - case RedmineKeys.TITLE: Title = reader.ReadElementContentAsString(); break; - - case RedmineKeys.SUMMARY: Summary = reader.ReadElementContentAsString(); break; - - case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; - - case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - default: reader.Read(); break; - } - } - } - - /// - /// - /// - /// - public void WriteXml(XmlWriter writer) { } - - /// - /// - /// - /// - /// - public bool Equals(News other) - { - if (other == null) return false; - return (Id == other.Id - && Project == other.Project - && Author == other.Author - && Title == other.Title - && Summary == other.Summary - && Description == other.Description - && CreatedOn == other.CreatedOn); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(Project, hashCode); - hashCode = HashCodeHelper.GetHashCode(Author, hashCode); - hashCode = HashCodeHelper.GetHashCode(Title, hashCode); - hashCode = HashCodeHelper.GetHashCode(Summary, hashCode); - hashCode = HashCodeHelper.GetHashCode(Description, hashCode); - hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); - return hashCode; - } - } - - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[News: {6}, Project={0}, Author={1}, Title={2}, Summary={3}, Description={4}, CreatedOn={5}]", - Project, Author, Title, Summary, Description, CreatedOn, base.ToString()); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/Permission.cs b/redmine-net20-api/Types/Permission.cs deleted file mode 100755 index 8bc8a75d..00000000 --- a/redmine-net20-api/Types/Permission.cs +++ /dev/null @@ -1,81 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Xml.Serialization; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// - /// - [XmlRoot(RedmineKeys.PERMISSION)] - public class Permission : IEquatable - { - /// - /// - /// - [XmlText] - public string Info { get; set; } - - /// - /// - /// - /// - /// - public bool Equals(Permission other) - { - return Info == other.Info; - } - - /// - /// - /// - /// - /// - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals(obj as Permission); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Info, hashCode); - return hashCode; - } - } - - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[Permission: Info={0}]", Info); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/Project.cs b/redmine-net20-api/Types/Project.cs deleted file mode 100644 index 91e83c8a..00000000 --- a/redmine-net20-api/Types/Project.cs +++ /dev/null @@ -1,284 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Xml; -using System.Xml.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 1.0 - /// - [XmlRoot(RedmineKeys.PROJECT)] - public class Project : IdentifiableName, IEquatable - { - /// - /// Gets or sets the identifier (Required). - /// - /// The identifier. - [XmlElement(RedmineKeys.IDENTIFIER)] - public String Identifier { get; set; } - - /// - /// Gets or sets the description. - /// - /// The description. - [XmlElement(RedmineKeys.DESCRIPTION)] - public String Description { get; set; } - - /// - /// Gets or sets the parent. - /// - /// The parent. - [XmlElement(RedmineKeys.PARENT)] - public IdentifiableName Parent { get; set; } - - /// - /// Gets or sets the home page. - /// - /// The home page. - [XmlElement(RedmineKeys.HOMEPAGE)] - public String HomePage { get; set; } - - /// - /// Gets or sets the created on. - /// - /// The created on. - [XmlElement(RedmineKeys.CREATED_ON, IsNullable = true)] - public DateTime? CreatedOn { get; set; } - - /// - /// Gets or sets the updated on. - /// - /// The updated on. - [XmlElement(RedmineKeys.UPDATED_ON, IsNullable = true)] - public DateTime? UpdatedOn { get; set; } - - /// - /// Gets or sets the status. - /// - /// - /// The status. - /// - [XmlElement(RedmineKeys.STATUS)] - public ProjectStatus Status { get; set; } - - /// - /// Gets or sets a value indicating whether this project is public. - /// - /// - /// true if this project is public; otherwise, false. - /// - /// is exposed since 2.6.0 - [XmlElement(RedmineKeys.IS_PUBLIC)] - public bool IsPublic { get; set; } - - /// - /// Gets or sets a value indicating whether [inherit members]. - /// - /// - /// true if [inherit members]; otherwise, false. - /// - [XmlElement(RedmineKeys.INHERIT_MEMBERS)] - public bool InheritMembers { get; set; } - - /// - /// Gets or sets the trackers. - /// - /// - /// The trackers. - /// - [XmlArray(RedmineKeys.TRACKERS)] - [XmlArrayItem(RedmineKeys.TRACKER)] - public IList Trackers { get; set; } - - /// - /// Gets or sets the custom fields. - /// - /// - /// The custom fields. - /// - [XmlArray(RedmineKeys.CUSTOM_FIELDS)] - [XmlArrayItem(RedmineKeys.CUSTOM_FIELD)] - public IList CustomFields { get; set; } - - /// - /// Gets or sets the issue categories. - /// - /// - /// The issue categories. - /// - [XmlArray(RedmineKeys.ISSUE_CATEGORIES)] - [XmlArrayItem(RedmineKeys.ISSUE_CATEGORY)] - public IList IssueCategories { get; set; } - - /// - /// since 2.6.0 - /// - /// - /// The enabled modules. - /// - [XmlArray(RedmineKeys.ENABLED_MODULES)] - [XmlArrayItem(RedmineKeys.ENABLED_MODULE)] - public IList EnabledModules { get; set; } - - [XmlArray(RedmineKeys.TIME_ENTRY_ACTIVITIES)] - [XmlArrayItem(RedmineKeys.TIME_ENTRY_ACTIVITY)] - public IList TimeEntryActivities { get; set; } - - /// - /// Generates an object from its XML representation. - /// - /// The stream from which the object is deserialized. - public override void ReadXml(XmlReader reader) - { - reader.Read(); - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; - - case RedmineKeys.IDENTIFIER: Identifier = reader.ReadElementContentAsString(); break; - - case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; - - case RedmineKeys.STATUS: Status = (ProjectStatus)reader.ReadElementContentAsInt(); break; - - case RedmineKeys.PARENT: Parent = new IdentifiableName(reader); break; - - case RedmineKeys.HOMEPAGE: HomePage = reader.ReadElementContentAsString(); break; - - case RedmineKeys.IS_PUBLIC: IsPublic = reader.ReadElementContentAsBoolean(); break; - - case RedmineKeys.INHERIT_MEMBERS: InheritMembers = reader.ReadElementContentAsBoolean(); break; - - case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case RedmineKeys.TRACKERS: Trackers = reader.ReadElementContentAsCollection(); break; - - case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadElementContentAsCollection(); break; - - case RedmineKeys.ISSUE_CATEGORIES: IssueCategories = reader.ReadElementContentAsCollection(); break; - - case RedmineKeys.ENABLED_MODULES: EnabledModules = reader.ReadElementContentAsCollection(); break; - - case RedmineKeys.TIME_ENTRY_ACTIVITIES: TimeEntryActivities = reader.ReadElementContentAsCollection(); break; - - default: reader.Read(); break; - } - } - } - - /// - /// - /// - public override void WriteXml(XmlWriter writer) - { - writer.WriteElementString(RedmineKeys.NAME, Name); - writer.WriteElementString(RedmineKeys.IDENTIFIER, Identifier); - writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); - writer.WriteElementString(RedmineKeys.INHERIT_MEMBERS, InheritMembers.ToString().ToLowerInvariant()); - writer.WriteElementString(RedmineKeys.IS_PUBLIC, IsPublic.ToString().ToLowerInvariant()); - writer.WriteIdOrEmpty(Parent, RedmineKeys.PARENT_ID); - writer.WriteElementString(RedmineKeys.HOMEPAGE, HomePage); - - writer.WriteListElements(Trackers as List, RedmineKeys.TRACKER_IDS); - writer.WriteListElements(EnabledModules as List, RedmineKeys.ENABLED_MODULE_NAMES); - - if (Id == 0) return; - - writer.WriteArray(CustomFields, RedmineKeys.CUSTOM_FIELDS); - } - - /// - /// - /// - /// - /// - public bool Equals(Project other) - { - if (other == null) return false; - return ( - Id == other.Id - && Identifier.Equals(other.Identifier) - && Description.Equals(other.Description) - && (Parent != null ? Parent.Equals(other.Parent) : other.Parent == null) - && (HomePage != null ? HomePage.Equals(other.HomePage) : other.HomePage == null) - && CreatedOn == other.CreatedOn - && UpdatedOn == other.UpdatedOn - && Status == other.Status - && IsPublic == other.IsPublic - && InheritMembers == other.InheritMembers - && (Trackers != null ? Trackers.Equals(other.Trackers) : other.Trackers == null) - && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null) - && (IssueCategories != null ? IssueCategories.Equals(other.IssueCategories) : other.IssueCategories == null) - && (EnabledModules != null ? EnabledModules.Equals(other.EnabledModules) : other.EnabledModules == null) - ); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(Identifier, hashCode); - hashCode = HashCodeHelper.GetHashCode(Description, hashCode); - hashCode = HashCodeHelper.GetHashCode(Parent, hashCode); - hashCode = HashCodeHelper.GetHashCode(HomePage, hashCode); - hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(Status, hashCode); - hashCode = HashCodeHelper.GetHashCode(IsPublic, hashCode); - hashCode = HashCodeHelper.GetHashCode(InheritMembers, hashCode); - hashCode = HashCodeHelper.GetHashCode(Trackers, hashCode); - hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); - hashCode = HashCodeHelper.GetHashCode(IssueCategories, hashCode); - hashCode = HashCodeHelper.GetHashCode(EnabledModules, hashCode); - - return hashCode; - } - } - - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[Project: {13}, Identifier={0}, Description={1}, Parent={2}, HomePage={3}, CreatedOn={4}, UpdatedOn={5}, Status={6}, IsPublic={7}, InheritMembers={8}, Trackers={9}, CustomFields={10}, IssueCategories={11}, EnabledModules={12}]", - Identifier, Description, Parent, HomePage, CreatedOn, UpdatedOn, Status, IsPublic, InheritMembers, Trackers, CustomFields, IssueCategories, EnabledModules, base.ToString()); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/ProjectEnabledModule.cs b/redmine-net20-api/Types/ProjectEnabledModule.cs deleted file mode 100755 index 77280e0e..00000000 --- a/redmine-net20-api/Types/ProjectEnabledModule.cs +++ /dev/null @@ -1,46 +0,0 @@ -ο»Ώ/* -Copyright 2011 - 2017 Adrian Popescu. - -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. -*/ - -using System.Xml.Serialization; - -namespace Redmine.Net.Api.Types -{ - /// - /// the module name: boards, calendar, documents, files, gantt, issue_tracking, news, repository, time_tracking, wiki. - /// - [XmlRoot(RedmineKeys.ENABLED_MODULE)] - public class ProjectEnabledModule : IdentifiableName, IValue - { - #region IValue implementation - /// - /// - /// - public string Value - { - get { return Name; } - } - - #endregion - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[ProjectEnabledModule: {0}]", base.ToString()); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/ProjectMembership.cs b/redmine-net20-api/Types/ProjectMembership.cs deleted file mode 100755 index 5e3fc5aa..00000000 --- a/redmine-net20-api/Types/ProjectMembership.cs +++ /dev/null @@ -1,159 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 1.4 - /// POST - Adds a project member. - /// GET - Returns the membership of given :id. - /// PUT - Updates the membership of given :id. Only the roles can be updated, the project and the user of a membership are read-only. - /// DELETE - Deletes a memberships. Memberships inherited from a group membership can not be deleted. You must delete the group membership. - /// - [XmlRoot(RedmineKeys.MEMBERSHIP)] - public class ProjectMembership : Identifiable, IEquatable, IXmlSerializable - { - /// - /// Gets or sets the project. - /// - /// The project. - [XmlElement(RedmineKeys.PROJECT)] - public IdentifiableName Project { get; set; } - - /// - /// Gets or sets the user. - /// - /// - /// The user. - /// - [XmlElement(RedmineKeys.USER)] - public IdentifiableName User { get; set; } - - /// - /// Gets or sets the group. - /// - /// - /// The group. - /// - [XmlElement(RedmineKeys.GROUP)] - public IdentifiableName Group { get; set; } - - /// - /// Gets or sets the type. - /// - /// The type. - [XmlArray(RedmineKeys.ROLES)] - [XmlArrayItem(RedmineKeys.ROLE)] - public List Roles { get; set; } - - /// - /// - /// - /// - /// - public bool Equals(ProjectMembership other) - { - if (other == null) return false; - return (Id == other.Id - && Project.Equals(other.Project) - && Roles.Equals(other.Roles) - && (User != null ? User.Equals(other.User) : other.User == null) - && (Group != null ? Group.Equals(other.Group) : other.Group == null)); - } - - /// - /// - /// - /// - public XmlSchema GetSchema() { return null; } - - /// - /// - /// - /// - public void ReadXml(XmlReader reader) - { - reader.Read(); - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; - - case RedmineKeys.USER: User = new IdentifiableName(reader); break; - - case RedmineKeys.GROUP: Group = new IdentifiableName(reader); break; - - case RedmineKeys.ROLES: Roles = reader.ReadElementContentAsCollection(); break; - - default: reader.Read(); break; - } - } - } - - /// - /// - /// - /// - public void WriteXml(XmlWriter writer) - { - writer.WriteIdIfNotNull(User, RedmineKeys.USER_ID); - writer.WriteArray(Roles, RedmineKeys.ROLE_IDS, typeof(MembershipRole), RedmineKeys.ROLE_ID); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(Project, hashCode); - hashCode = HashCodeHelper.GetHashCode(User, hashCode); - hashCode = HashCodeHelper.GetHashCode(Group, hashCode); - hashCode = HashCodeHelper.GetHashCode(Roles, hashCode); - return hashCode; - } - } - - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[ProjectMembership: {4}, Project={0}, User={1}, Group={2}, Roles={3}]", Project, User, Group, Roles, base.ToString()); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/Query.cs b/redmine-net20-api/Types/Query.cs deleted file mode 100755 index 6d664bbe..00000000 --- a/redmine-net20-api/Types/Query.cs +++ /dev/null @@ -1,119 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Xml; -using System.Xml.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 1.3 - /// - [XmlRoot(RedmineKeys.QUERY)] - public class Query : IdentifiableName, IEquatable - { - /// - /// Gets or sets a value indicating whether this instance is public. - /// - /// true if this instance is public; otherwise, false. - [XmlElement(RedmineKeys.IS_PUBLIC)] - public bool IsPublic { get; set; } - - /// - /// Gets or sets the project id. - /// - /// The project id. - [XmlElement(RedmineKeys.PROJECT_ID)] - public int? ProjectId { get; set; } - - /// - /// - /// - /// - public override void ReadXml(XmlReader reader) - { - reader.Read(); - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; - - case RedmineKeys.IS_PUBLIC: IsPublic = reader.ReadElementContentAsBoolean(); break; - - case RedmineKeys.PROJECT_ID: ProjectId = reader.ReadElementContentAsNullableInt(); break; - - default: reader.Read(); break; - } - } - } - - /// - /// - /// - /// - public override void WriteXml(XmlWriter writer) { } - - /// - /// - /// - /// - /// - public bool Equals(Query other) - { - if (other == null) return false; - - return (other.Id == Id && other.Name == Name && other.IsPublic == IsPublic && other.ProjectId == ProjectId); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); - hashCode = HashCodeHelper.GetHashCode(IsPublic, hashCode); - hashCode = HashCodeHelper.GetHashCode(ProjectId, hashCode); - return hashCode; - } - } - - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[Query: {2}, IsPublic={0}, ProjectId={1}]", IsPublic, ProjectId, base.ToString()); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/Role.cs b/redmine-net20-api/Types/Role.cs deleted file mode 100755 index 04f86b81..00000000 --- a/redmine-net20-api/Types/Role.cs +++ /dev/null @@ -1,125 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Xml; -using System.Xml.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 1.4 - /// - [XmlRoot(RedmineKeys.ROLE)] - public class Role : IdentifiableName, IEquatable - { - /// - /// Gets or sets the permissions. - /// - /// - /// The issue relations. - /// - [XmlArray(RedmineKeys.PERMISSIONS)] - [XmlArrayItem(RedmineKeys.PERMISSION)] - public IList Permissions { get; set; } - - /// - /// - /// - /// - public override void ReadXml(XmlReader reader) - { - reader.Read(); - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; - - case RedmineKeys.PERMISSIONS: Permissions = reader.ReadElementContentAsCollection(); break; - - default: reader.Read(); break; - } - } - } - - /// - /// - /// - /// - public override void WriteXml(XmlWriter writer) { } - - /// - /// - /// - /// - /// - public bool Equals(Role other) - { - if (other == null) return false; - return Id == other.Id && Name == other.Name; - } - - /// - /// - /// - /// - /// - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals(obj as Role); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); - hashCode = HashCodeHelper.GetHashCode(Permissions, hashCode); - return hashCode; - } - } - - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[Role: Id={0}, Name={1}, Permissions={2}]", Id, Name, Permissions); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/TimeEntry.cs b/redmine-net20-api/Types/TimeEntry.cs deleted file mode 100755 index 80e32e0d..00000000 --- a/redmine-net20-api/Types/TimeEntry.cs +++ /dev/null @@ -1,263 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Internals; - - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 1.1 - /// - [XmlRoot(RedmineKeys.TIME_ENTRY)] - public class TimeEntry : Identifiable, ICloneable, IEquatable, IXmlSerializable - { - private string comments; - - /// - /// Gets or sets the issue id to log time on. - /// - /// The issue id. - [XmlAttribute(RedmineKeys.ISSUE)] - public IdentifiableName Issue { get; set; } - - /// - /// Gets or sets the project id to log time on. - /// - /// The project id. - [XmlAttribute(RedmineKeys.PROJECT)] - public IdentifiableName Project { get; set; } - - /// - /// Gets or sets the date the time was spent (default to the current date). - /// - /// The spent on. - [XmlAttribute(RedmineKeys.SPENT_ON)] - public DateTime? SpentOn { get; set; } - - /// - /// Gets or sets the number of spent hours. - /// - /// The hours. - [XmlAttribute(RedmineKeys.HOURS)] - public decimal Hours { get; set; } - - /// - /// Gets or sets the activity id of the time activity. This parameter is required unless a default activity is defined in Redmine.. - /// - /// The activity id. - [XmlAttribute(RedmineKeys.ACTIVITY)] - public IdentifiableName Activity { get; set; } - - /// - /// Gets or sets the user. - /// - /// - /// The user. - /// - [XmlAttribute(RedmineKeys.USER)] - public IdentifiableName User { get; set; } - - /// - /// Gets or sets the short description for the entry (255 characters max). - /// - /// The comments. - [XmlAttribute(RedmineKeys.COMMENTS)] - public String Comments - { - get { return comments; } - set - { - if (!string.IsNullOrEmpty(value)) - { - if (value.Length > 255) - { - value = value.Substring(0, 255); - } - } - comments = value; - } - } - - /// - /// Gets or sets the created on. - /// - /// The created on. - [XmlElement(RedmineKeys.CREATED_ON)] - public DateTime? CreatedOn { get; set; } - - /// - /// Gets or sets the updated on. - /// - /// The updated on. - [XmlElement(RedmineKeys.UPDATED_ON)] - public DateTime? UpdatedOn { get; set; } - - /// - /// Gets or sets the custom fields. - /// - /// The custom fields. - [XmlArray(RedmineKeys.CUSTOM_FIELDS)] - [XmlArrayItem(RedmineKeys.CUSTOM_FIELD)] - public IList CustomFields { get; set; } - - /// - /// - /// - /// - public object Clone() - { - var timeEntry = new TimeEntry { Activity = Activity, Comments = Comments, Hours = Hours, Issue = Issue, Project = Project, SpentOn = SpentOn, User = User, CustomFields = CustomFields }; - return timeEntry; - } - - /// - /// - /// - /// - public XmlSchema GetSchema() { return null; } - - /// - /// - /// - /// - public void ReadXml(XmlReader reader) - { - reader.Read(); - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.ISSUE_ID: Issue = new IdentifiableName(reader); break; - - case RedmineKeys.ISSUE: Issue = new IdentifiableName(reader); break; - - case RedmineKeys.PROJECT_ID: Project = new IdentifiableName(reader); break; - - case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; - - case RedmineKeys.SPENT_ON: SpentOn = reader.ReadElementContentAsNullableDateTime(); break; - - case RedmineKeys.USER: User = new IdentifiableName(reader); break; - - case RedmineKeys.HOURS: Hours = reader.ReadElementContentAsDecimal(); break; - - case RedmineKeys.ACTIVITY_ID: Activity = new IdentifiableName(reader); break; - - case RedmineKeys.ACTIVITY: Activity = new IdentifiableName(reader); break; - - case RedmineKeys.COMMENTS: Comments = reader.ReadElementContentAsString(); break; - - case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadElementContentAsCollection(); break; - - default: reader.Read(); break; - } - } - } - - /// - /// - /// - /// - public void WriteXml(XmlWriter writer) - { - writer.WriteIdIfNotNull(Issue, RedmineKeys.ISSUE_ID); - writer.WriteIdIfNotNull(Project, RedmineKeys.PROJECT_ID); - - if (!SpentOn.HasValue) - SpentOn = DateTime.Now; - - writer.WriteDateOrEmpty(SpentOn, RedmineKeys.SPENT_ON); - writer.WriteValueOrEmpty(Hours, RedmineKeys.HOURS); - writer.WriteIdIfNotNull(Activity, RedmineKeys.ACTIVITY_ID); - writer.WriteElementString(RedmineKeys.COMMENTS, Comments); - - writer.WriteArray(CustomFields, RedmineKeys.CUSTOM_FIELDS); - } - - /// - /// - /// - /// - /// - public bool Equals(TimeEntry other) - { - if (other == null) return false; - return (Id == other.Id - && Issue == other.Issue - && Project == other.Project - && SpentOn == other.SpentOn - && Hours == other.Hours - && Activity == other.Activity - && Comments == other.Comments - && User == other.User - && CreatedOn == other.CreatedOn - && UpdatedOn == other.UpdatedOn - && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null)); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(Issue, hashCode); - hashCode = HashCodeHelper.GetHashCode(Project, hashCode); - hashCode = HashCodeHelper.GetHashCode(SpentOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(Hours, hashCode); - hashCode = HashCodeHelper.GetHashCode(Activity, hashCode); - hashCode = HashCodeHelper.GetHashCode(User, hashCode); - hashCode = HashCodeHelper.GetHashCode(Comments, hashCode); - hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); - return hashCode; - } - } - - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[TimeEntry: {10}, Issue={0}, Project={1}, SpentOn={2}, Hours={3}, Activity={4}, User={5}, Comments={6}, CreatedOn={7}, UpdatedOn={8}, CustomFields={9}]", - Issue, Project, SpentOn, Hours, Activity, User, Comments, CreatedOn, UpdatedOn, CustomFields, base.ToString()); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/TimeEntryActivity.cs b/redmine-net20-api/Types/TimeEntryActivity.cs deleted file mode 100755 index 971b96ac..00000000 --- a/redmine-net20-api/Types/TimeEntryActivity.cs +++ /dev/null @@ -1,128 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Xml; -using System.Xml.Serialization; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 2.2 - /// - [XmlRoot(RedmineKeys.TIME_ENTRY_ACTIVITY)] - public class TimeEntryActivity : IdentifiableName, IEquatable - { - /// - /// - /// - [XmlElement(RedmineKeys.IS_DEFAULT)] - public bool IsDefault { get; set; } - - #region Implementation of IXmlSerializable - - /// - /// Generates an object from its XML representation. - /// - /// The stream from which the object is deserialized. - public override void ReadXml(XmlReader reader) - { - reader.Read(); - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; - - case RedmineKeys.IS_DEFAULT: IsDefault = reader.ReadElementContentAsBoolean(); break; - - default: reader.Read(); break; - } - } - } - - /// - /// - /// - /// - public override void WriteXml(XmlWriter writer) { } - - #endregion - - #region Implementation of IEquatable - - /// - /// - /// - /// - /// - public bool Equals(TimeEntryActivity other) - { - if (other == null) return false; - - return Id == other.Id && Name == other.Name && IsDefault == other.IsDefault; - } - - /// - /// - /// - /// - /// - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals(obj as TimeEntryActivity); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); - hashCode = HashCodeHelper.GetHashCode(IsDefault, hashCode); - return hashCode; - } - } - - #endregion - - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[TimeEntryActivity: Id={0}, Name={1}, IsDefault={2}]", Id, Name, IsDefault); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/Tracker.cs b/redmine-net20-api/Types/Tracker.cs deleted file mode 100755 index c68958a4..00000000 --- a/redmine-net20-api/Types/Tracker.cs +++ /dev/null @@ -1,112 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Xml; -using System.Xml.Serialization; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 1.3 - /// - [XmlRoot(RedmineKeys.TRACKER)] - public class Tracker : IdentifiableName, IEquatable - { - /// - /// - /// - public override void WriteXml(XmlWriter writer) { } - - /// - /// Generates an object from its XML representation. - /// - /// The stream from which the object is deserialized. - public override void ReadXml(XmlReader reader) - { - reader.Read(); - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; - - default: reader.Read(); break; - } - } - } - - /// - /// Indicates whether the current object is equal to another object of the same type. - /// - /// An object to compare with this object. - /// - /// true if the current object is equal to the parameter; otherwise, false. - /// - public bool Equals(Tracker other) - { - if (other == null) return false; - - return Id == other.Id && Name == other.Name; - } - - /// - /// - /// - /// - /// - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals(obj as Tracker); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); - return hashCode; - } - } - - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[Tracker: Id={0}, Name={1}]", Id, Name); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/Upload.cs b/redmine-net20-api/Types/Upload.cs deleted file mode 100755 index 81005063..00000000 --- a/redmine-net20-api/Types/Upload.cs +++ /dev/null @@ -1,120 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Xml.Schema; -using System.Xml.Serialization; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// Support for adding attachments through the REST API is added in Redmine 1.4.0. - /// - [XmlRoot(RedmineKeys.UPLOAD)] - public class Upload : IEquatable - { - /// - /// Gets or sets the uploaded token. - /// - /// The name of the file. - [XmlElement(RedmineKeys.TOKEN)] - public string Token { get; set; } - - /// - /// Gets or sets the name of the file. - /// Maximum allowed file size (1024000). - /// - /// The name of the file. - [XmlElement(RedmineKeys.FILENAME)] - public string FileName { get; set; } - - /// - /// Gets or sets the name of the file. - /// - /// The name of the file. - [XmlElement(RedmineKeys.CONTENT_TYPE)] - public string ContentType { get; set; } - - /// - /// Gets or sets the file description. (Undocumented feature) - /// - /// The file descroΓΌtopm. - [XmlElement(RedmineKeys.DESCRIPTION)] - public string Description { get; set; } - - /// - /// - /// - /// - public XmlSchema GetSchema() { return null; } - - /// - /// Indicates whether the current object is equal to another object of the same type. - /// - /// An object to compare with this object. - /// - /// true if the current object is equal to the parameter; otherwise, false. - /// - public bool Equals(Upload other) - { - return other != null - && Token.Equals(other.Token) - && FileName.Equals(other.FileName) - && Description.Equals(other.Description) - && ContentType.Equals(other.ContentType); - } - - /// - /// - /// - /// - /// - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals(obj as Upload); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Token, hashCode); - hashCode = HashCodeHelper.GetHashCode(FileName, hashCode); - hashCode = HashCodeHelper.GetHashCode(Description, hashCode); - hashCode = HashCodeHelper.GetHashCode(ContentType, hashCode); - return hashCode; - } - } - - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[Upload: Token={0}, FileName={1}, ContentType={2}, Description={3}]", Token, FileName, ContentType, Description); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/User.cs b/redmine-net20-api/Types/User.cs deleted file mode 100755 index fd7cc476..00000000 --- a/redmine-net20-api/Types/User.cs +++ /dev/null @@ -1,291 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 1.1 - /// - [XmlRoot(RedmineKeys.USER)] - public class User : Identifiable, IXmlSerializable, IEquatable - { - /// - /// Gets or sets the user login. - /// - /// The login. - [XmlElement(RedmineKeys.LOGIN)] - public String Login { get; set; } - - /// - /// Gets or sets the user password. - /// - /// The password. - [XmlElement(RedmineKeys.PASSWORD)] - public string Password { get; set; } - - /// - /// Gets or sets the first name. - /// - /// The first name. - [XmlElement(RedmineKeys.FIRSTNAME)] - public String FirstName { get; set; } - - /// - /// Gets or sets the last name. - /// - /// The last name. - [XmlElement(RedmineKeys.LASTNAME)] - public String LastName { get; set; } - - /// - /// Gets or sets the email. - /// - /// The email. - [XmlElement(RedmineKeys.MAIL)] - public String Email { get; set; } - - /// - /// Gets or sets the authentication mode id. - /// - /// - /// The authentication mode id. - /// - [XmlElement(RedmineKeys.AUTH_SOURCE_ID, IsNullable = true)] - public Int32? AuthenticationModeId { get; set; } - - /// - /// Gets or sets the created on. - /// - /// The created on. - [XmlElement(RedmineKeys.CREATED_ON, IsNullable = true)] - public DateTime? CreatedOn { get; set; } - - /// - /// Gets or sets the last login on. - /// - /// The last login on. - [XmlElement(RedmineKeys.LAST_LOGIN_ON, IsNullable = true)] - public DateTime? LastLoginOn { get; set; } - - /// - /// Gets the API key of the user, visible for admins and for yourself (added in 2.3.0) - /// - [XmlElement(RedmineKeys.API_KEY, IsNullable = true)] - public string ApiKey { get; set; } - - /// - /// Gets the status of the user, visible for admins only (added in 2.4.0) - /// - [XmlElement(RedmineKeys.STATUS, IsNullable = true)] - public UserStatus Status { get; set; } - - /// - /// - /// - [XmlElement(RedmineKeys.MUST_CHANGE_PASSWD, IsNullable = true)] - public bool MustChangePassword { get; set; } - - /// - /// Gets or sets the custom fields. - /// - /// The custom fields. - [XmlArray(RedmineKeys.CUSTOM_FIELDS)] - [XmlArrayItem(RedmineKeys.CUSTOM_FIELD)] - public List CustomFields { get; set; } - - /// - /// Gets or sets the memberships. - /// - /// - /// The memberships. - /// - [XmlArray(RedmineKeys.MEMBERSHIPS)] - [XmlArrayItem(RedmineKeys.MEMBERSHIP)] - public List Memberships { get; set; } - - /// - /// Gets or sets the user's groups. - /// - /// - /// The groups. - /// - [XmlArray(RedmineKeys.GROUPS)] - [XmlArrayItem(RedmineKeys.GROUP)] - public List Groups { get; set; } - - /// - /// Gets or sets the user's mail_notification. - /// - /// - /// only_my_events, only_assigned, [...] - /// - [XmlElement(RedmineKeys.MAIL_NOTIFICATION)] - public string MailNotification { get; set; } - - /// - /// - /// - /// - public XmlSchema GetSchema() - { - return null; - } - - /// - /// - /// - /// - public void ReadXml(XmlReader reader) - { - reader.Read(); - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.LOGIN: Login = reader.ReadElementContentAsString(); break; - - case RedmineKeys.FIRSTNAME: FirstName = reader.ReadElementContentAsString(); break; - - case RedmineKeys.LASTNAME: LastName = reader.ReadElementContentAsString(); break; - - case RedmineKeys.MAIL: Email = reader.ReadElementContentAsString(); break; - - case RedmineKeys.MAIL_NOTIFICATION: MailNotification = reader.ReadElementContentAsString(); break; - - case RedmineKeys.MUST_CHANGE_PASSWD: MustChangePassword = reader.ReadElementContentAsBoolean(); break; - - case RedmineKeys.AUTH_SOURCE_ID: AuthenticationModeId = reader.ReadElementContentAsNullableInt(); break; - - case RedmineKeys.LAST_LOGIN_ON: LastLoginOn = reader.ReadElementContentAsNullableDateTime(); break; - - case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case RedmineKeys.API_KEY: ApiKey = reader.ReadElementContentAsString(); break; - - case RedmineKeys.STATUS: Status = (UserStatus)reader.ReadElementContentAsInt(); break; - - case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadElementContentAsCollection(); break; - - case RedmineKeys.MEMBERSHIPS: Memberships = reader.ReadElementContentAsCollection(); break; - - case RedmineKeys.GROUPS: Groups = reader.ReadElementContentAsCollection(); break; - - default: reader.Read(); break; - } - } - } - - /// - /// - /// - /// - public void WriteXml(XmlWriter writer) - { - writer.WriteElementString(RedmineKeys.LOGIN, Login); - writer.WriteElementString(RedmineKeys.FIRSTNAME, FirstName); - writer.WriteElementString(RedmineKeys.LASTNAME, LastName); - writer.WriteElementString(RedmineKeys.MAIL, Email); - writer.WriteElementString(RedmineKeys.MAIL_NOTIFICATION, MailNotification); - writer.WriteElementString(RedmineKeys.PASSWORD, Password); - writer.WriteValueOrEmpty(AuthenticationModeId, RedmineKeys.AUTH_SOURCE_ID); - writer.WriteElementString(RedmineKeys.MUST_CHANGE_PASSWD, MustChangePassword.ToString().ToLowerInvariant()); - - writer.WriteArray(CustomFields, RedmineKeys.CUSTOM_FIELDS); - } - - /// - /// - /// - /// - /// - public bool Equals(User other) - { - if (other == null) return false; - return ( - Id == other.Id - && Login.Equals(other.Login) - //&& Password.Equals(other.Password) - && FirstName.Equals(other.FirstName) - && LastName.Equals(other.LastName) - && Email.Equals(other.Email) - && MailNotification.Equals(other.MailNotification) - && (ApiKey != null ? ApiKey.Equals(other.ApiKey) : other.ApiKey == null) - && AuthenticationModeId == other.AuthenticationModeId - && CreatedOn == other.CreatedOn - && LastLoginOn == other.LastLoginOn - && Status == other.Status - && MustChangePassword == other.MustChangePassword - && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null) - && (Memberships != null ? Memberships.Equals(other.Memberships): other.Memberships == null) - && (Groups != null ? Groups.Equals(other.Groups) : other.Groups == null) - ); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(Login, hashCode); - hashCode = HashCodeHelper.GetHashCode(Password, hashCode); - hashCode = HashCodeHelper.GetHashCode(FirstName, hashCode); - hashCode = HashCodeHelper.GetHashCode(LastName, hashCode); - hashCode = HashCodeHelper.GetHashCode(Email, hashCode); - hashCode = HashCodeHelper.GetHashCode(MailNotification, hashCode); - hashCode = HashCodeHelper.GetHashCode(AuthenticationModeId, hashCode); - hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(LastLoginOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(ApiKey, hashCode); - hashCode = HashCodeHelper.GetHashCode(Status, hashCode); - hashCode = HashCodeHelper.GetHashCode(MustChangePassword, hashCode); - hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); - hashCode = HashCodeHelper.GetHashCode(Memberships, hashCode); - hashCode = HashCodeHelper.GetHashCode(Groups, hashCode); - return hashCode; - } - } - - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[User: {14}, Login={0}, Password={1}, FirstName={2}, LastName={3}, Email={4}, EmailNotification={5}, AuthenticationModeId={6}, CreatedOn={7}, LastLoginOn={8}, ApiKey={9}, Status={10}, MustChangePassword={11}, CustomFields={12}, Memberships={13}, Groups={14}]", - Login, Password, FirstName, LastName, Email, MailNotification, AuthenticationModeId, CreatedOn, LastLoginOn, ApiKey, Status, MustChangePassword, CustomFields, Memberships, Groups, base.ToString()); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/Version.cs b/redmine-net20-api/Types/Version.cs deleted file mode 100755 index f0ad797e..00000000 --- a/redmine-net20-api/Types/Version.cs +++ /dev/null @@ -1,241 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Xml; -using System.Xml.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 1.3 - /// - [XmlRoot(RedmineKeys.VERSION)] - public class Version : IdentifiableName, IEquatable - { - /// - /// Gets or sets the project. - /// - /// The project. - [XmlElement(RedmineKeys.PROJECT)] - public IdentifiableName Project { get; set; } - - /// - /// Gets or sets the description. - /// - /// The description. - [XmlElement(RedmineKeys.DESCRIPTION)] - public String Description { get; set; } - - /// - /// Gets or sets the status. - /// - /// The status. - [XmlElement(RedmineKeys.STATUS)] - public VersionStatus Status { get; set; } - - /// - /// Gets or sets the due date. - /// - /// The due date. - [XmlElement(RedmineKeys.DUE_DATE, IsNullable = true)] - public DateTime? DueDate { get; set; } - - /// - /// Gets or sets the sharing. - /// - /// The sharing. - [XmlElement(RedmineKeys.SHARING)] - public VersionSharing Sharing { get; set; } - - /// - /// Gets or sets the created on. - /// - /// The created on. - [XmlElement(RedmineKeys.CREATED_ON, IsNullable = true)] - public DateTime? CreatedOn { get; set; } - - /// - /// Gets or sets the updated on. - /// - /// The updated on. - [XmlElement(RedmineKeys.UPDATED_ON, IsNullable = true)] - public DateTime? UpdatedOn { get; set; } - - /// - /// Gets or sets the custom fields. - /// - /// The custom fields. - [XmlArray(RedmineKeys.CUSTOM_FIELDS)] - [XmlArrayItem(RedmineKeys.CUSTOM_FIELD)] - public IList CustomFields { get; set; } - - /// - /// - /// - /// - public override void ReadXml(XmlReader reader) - { - reader.Read(); - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; - - case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; - - case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; - - case RedmineKeys.STATUS: Status = (VersionStatus)Enum.Parse(typeof(VersionStatus), reader.ReadElementContentAsString(), true); break; - - case RedmineKeys.DUE_DATE: DueDate = reader.ReadElementContentAsNullableDateTime(); break; - - case RedmineKeys.SHARING: Sharing = (VersionSharing)Enum.Parse(typeof(VersionSharing), reader.ReadElementContentAsString(), true); break; - - case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadElementContentAsCollection(); break; - - default: reader.Read(); break; - } - } - } - - /// - /// - /// - /// - public override void WriteXml(XmlWriter writer) - { - writer.WriteElementString(RedmineKeys.NAME, Name); - writer.WriteElementString(RedmineKeys.STATUS, Status.ToString().ToLowerInvariant()); - writer.WriteElementString(RedmineKeys.SHARING, Sharing.ToString().ToLowerInvariant()); - - writer.WriteDateOrEmpty(DueDate, RedmineKeys.DUE_DATE); - writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); - } - - /// - /// - /// - /// - /// - public bool Equals(Version other) - { - if (other == null) return false; - return (Id == other.Id && Name == other.Name - && Project == other.Project - && Description == other.Description - && Status == other.Status - && DueDate == other.DueDate - && Sharing == other.Sharing - && CreatedOn == other.CreatedOn - && UpdatedOn == other.UpdatedOn - && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null)); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(Project, hashCode); - hashCode = HashCodeHelper.GetHashCode(Description, hashCode); - hashCode = HashCodeHelper.GetHashCode(Status, hashCode); - hashCode = HashCodeHelper.GetHashCode(DueDate, hashCode); - hashCode = HashCodeHelper.GetHashCode(Sharing, hashCode); - hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); - return hashCode; - } - } - - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[Version: {8}, Project={0}, Description={1}, Status={2}, DueDate={3}, Sharing={4}, CreatedOn={5}, UpdatedOn={6}, CustomFields={7}]", - Project, Description, Status, DueDate, Sharing, CreatedOn, UpdatedOn, CustomFields, base.ToString()); - } - } - - /// - /// - /// - public enum VersionSharing - { - /// - /// - /// - none = 1, - /// - /// - /// - descendants, - /// - /// - /// - hierarchy, - /// - /// - /// - tree, - /// - /// - /// - system - } - - /// - /// - /// - public enum VersionStatus - { - /// - /// - /// - open = 1, - /// - /// - /// - locked, - /// - /// - /// - closed - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/WikiPage.cs b/redmine-net20-api/Types/WikiPage.cs deleted file mode 100644 index f06fb41e..00000000 --- a/redmine-net20-api/Types/WikiPage.cs +++ /dev/null @@ -1,214 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 2.2 - /// - [XmlRoot(RedmineKeys.WIKI_PAGE)] - public class WikiPage : Identifiable, IXmlSerializable, IEquatable - { - /// - /// - /// - [XmlElement(RedmineKeys.TITLE)] - public string Title { get; set; } - - /// - /// - /// - [XmlElement(RedmineKeys.TEXT)] - public string Text { get; set; } - - /// - /// - /// - [XmlElement(RedmineKeys.COMMENTS)] - public string Comments { get; set; } - - /// - /// - /// - [XmlElement(RedmineKeys.VERSION)] - public int Version { get; set; } - - /// - /// - /// - [XmlElement(RedmineKeys.AUTHOR)] - public IdentifiableName Author { get; set; } - - /// - /// Gets or sets the created on. - /// - /// The created on. - [XmlElement(RedmineKeys.CREATED_ON)] - public DateTime? CreatedOn { get; set; } - - /// - /// Gets or sets the updated on. - /// - /// The updated on. - [XmlElement(RedmineKeys.UPDATED_ON)] - public DateTime? UpdatedOn { get; set; } - - /// - /// Gets or sets the attachments. - /// - /// - /// The attachments. - /// - [XmlArray(RedmineKeys.ATTACHMENTS)] - [XmlArrayItem(RedmineKeys.ATTACHMENT)] - public IList Attachments { get; set; } - - /// - /// Sets the uploads. - /// - /// - /// The uploads. - /// - /// Availability starting with redmine version 3.3 - [XmlArray(RedmineKeys.UPLOADS)] - [XmlArrayItem(RedmineKeys.UPLOAD)] - public IList Uploads { get; set; } - - #region Implementation of IXmlSerializable - - /// - /// - /// - /// - public XmlSchema GetSchema() { return null; } - - /// - /// - /// - /// - public void ReadXml(XmlReader reader) - { - reader.Read(); - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.TITLE: Title = reader.ReadElementContentAsString(); break; - - case RedmineKeys.TEXT: Text = reader.ReadElementContentAsString(); break; - - case RedmineKeys.COMMENTS: Comments = reader.ReadElementContentAsString(); break; - - case RedmineKeys.VERSION: Version = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; - - case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case RedmineKeys.ATTACHMENTS: Attachments = reader.ReadElementContentAsCollection(); break; - - default: reader.Read(); break; - } - } - } - - /// - /// - /// - /// - public void WriteXml(XmlWriter writer) - { - writer.WriteElementString(RedmineKeys.TEXT, Text); - writer.WriteElementString(RedmineKeys.COMMENTS, Comments); - writer.WriteValueOrEmpty(Version, RedmineKeys.VERSION); - writer.WriteArray(Uploads, RedmineKeys.UPLOADS); - } - - #endregion - - #region Implementation of IEquatable - - /// - /// - /// - /// - /// - public bool Equals(WikiPage other) - { - if (other == null) return false; - - return Id == other.Id - && Title == other.Title - && Text == other.Text - && Comments == other.Comments - && Version == other.Version - && Author == other.Author - && CreatedOn == other.CreatedOn - && UpdatedOn == other.UpdatedOn; - } - - /// - /// - /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(Title, hashCode); - hashCode = HashCodeHelper.GetHashCode(Text, hashCode); - hashCode = HashCodeHelper.GetHashCode(Comments, hashCode); - hashCode = HashCodeHelper.GetHashCode(Version, hashCode); - hashCode = HashCodeHelper.GetHashCode(Author, hashCode); - hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(Attachments, hashCode); - return hashCode; - } - } - - /// - /// - /// - /// - public override string ToString() - { - return string.Format("[WikiPage: {8}, Title={0}, Text={1}, Comments={2}, Version={3}, Author={4}, CreatedOn={5}, UpdatedOn={6}, Attachments={7}]", - Title, Text, Comments, Version, Author, CreatedOn, UpdatedOn, Attachments, base.ToString()); - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net20-api/redmine-net20-api.csproj b/redmine-net20-api/redmine-net20-api.csproj deleted file mode 100644 index a745a7c6..00000000 --- a/redmine-net20-api/redmine-net20-api.csproj +++ /dev/null @@ -1,163 +0,0 @@ -ο»Ώ - - - - Debug - AnyCPU - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD} - Library - Properties - Redmine.Net.Api - redmine-net20-api - v2.0 - 512 - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\redmine-net20-api.XML - - - none - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\redmine-net20-api.XML - - - true - bin\DebugXML\ - DEBUG;TRACE - full - AnyCPU - prompt - MinimumRecommendedRules.ruleset - 4 - false - - - true - bin\DebugJSON\ - DEBUG;TRACE - full - AnyCPU - prompt - MinimumRecommendedRules.ruleset - 4 - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Component - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/redmine-net40-api-signed/Properties/AssemblyInfo.cs b/redmine-net40-api-signed/Properties/AssemblyInfo.cs deleted file mode 100755 index 8c614b20..00000000 --- a/redmine-net40-api-signed/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -ο»Ώusing System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("redmine-net40-api-signed")] -[assembly: AssemblyDescription("redmine-net-api is a library for communicating with a Redmine project management application.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("redmine-net40-api-signed")] -[assembly: AssemblyCopyright("Copyright Β© Adrian Popescu 2011 - 2017")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("034d1a12-8c7a-4875-a9cf-a789ea0cf96f")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.4")] -[assembly: AssemblyFileVersion("1.0.4")] diff --git a/redmine-net40-api-signed/redmine-net40-api-signed.csproj b/redmine-net40-api-signed/redmine-net40-api-signed.csproj deleted file mode 100644 index 5245331d..00000000 --- a/redmine-net40-api-signed/redmine-net40-api-signed.csproj +++ /dev/null @@ -1,470 +0,0 @@ -ο»Ώ - - - - Debug - AnyCPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E} - Library - Properties - Redmine.Net.Api - redmine-net40-api-signed - v4.0 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\redmine-net40-api-signed.XML - - - none - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\redmine-net40-api-signed.XML - - - true - - - redmine-net-api.snk - - - true - bin\DebugXML\ - DEBUG;TRACE - full - AnyCPU - prompt - MinimumRecommendedRules.ruleset - 4 - false - - - true - bin\DebugJSON\ - DEBUG;TRACE - full - AnyCPU - prompt - MinimumRecommendedRules.ruleset - 4 - false - - - - - - - - - - - - - - Internals\HashCodeHelper.cs - - - Internals\DataHelper.cs - - - Extensions\XmlWriterExtensions.cs - - - Extensions\NameValueCollectionExtensions.cs - - - HttpVerbs.cs - - - Extensions\XmlReaderExtensions.cs - - - Internals\UrlHelper.cs - - - Internals\WebApiHelper.cs - - - Logging\ColorConsoleLogger.cs - - - Logging\ConsoleLogger.cs - - - Logging\ILogger.cs - - - Logging\LogEntry.cs - - - Logging\Logger.cs - - - Logging\LoggerExtensions.cs - - - Logging\LoggingEventType.cs - - - Logging\RedmineConsoleTraceListener.cs - - - Logging\TraceLogger.cs - - - RedmineKeys.cs - - - RedmineManager.cs - - - RedmineWebClient.cs - Component - - - Types\Attachment.cs - - - Types\Attachments.cs - - - Types\ChangeSet.cs - - - Types\CustomField.cs - - - Types\CustomFieldPossibleValue.cs - - - Types\CustomFieldRole.cs - - - Types\CustomFieldValue.cs - - - Types\Detail.cs - - - Types\Error.cs - - - Types\File.cs - - - Types\Group.cs - - - Types\GroupUser.cs - - - Types\Identifiable.cs - - - Types\IdentifiableName.cs - - - Types\IRedmineManager.cs - - - Types\IRedmineWebClient.cs - - - Types\Issue.cs - - - Types\IssueCategory.cs - - - Types\IssueChild.cs - - - Types\IssueCustomField.cs - - - Types\IssuePriority.cs - - - Types\IssueRelation.cs - - - Types\IssueRelationType.cs - - - Types\IssueStatus.cs - - - Types\IValue.cs - - - Types\Journal.cs - - - Types\Membership.cs - - - Types\MembershipRole.cs - - - Types\News.cs - - - Types\PaginatedObjects.cs - - - Types\Permission.cs - - - Types\Project.cs - - - Types\ProjectEnabledModule.cs - - - Types\ProjectIssueCategory.cs - - - Types\ProjectMembership.cs - - - Types\ProjectStatus.cs - - - Types\ProjectTracker.cs - - - Types\Query.cs - - - Types\Role.cs - - - Types\TimeEntry.cs - - - Types\TimeEntryActivity.cs - - - Types\Tracker.cs - - - Types\TrackerCustomField.cs - - - Types\Upload.cs - - - Types\User.cs - - - Types\UserGroup.cs - - - Types\UserStatus.cs - - - Types\Version.cs - - - Types\Watcher.cs - - - Types\WikiPage.cs - - - Async\RedmineManagerAsync.cs - - - Extensions\CollectionExtensions.cs - - - Extensions\JsonExtensions.cs - - - Extensions\WebExtensions.cs - - - Internals\RedmineSerializer.cs - - - Internals\RedmineSerializerJson.cs - - - JsonConverters\AttachmentConverter.cs - - - JsonConverters\AttachmentsConverter.cs - - - JsonConverters\ChangeSetConverter.cs - - - JsonConverters\CustomFieldConverter.cs - - - JsonConverters\CustomFieldPossibleValueConverter.cs - - - JsonConverters\CustomFieldRoleConverter.cs - - - JsonConverters\DetailConverter.cs - - - JsonConverters\ErrorConverter.cs - - - JsonConverters\FileConverter.cs - - - JsonConverters\GroupConverter.cs - - - JsonConverters\GroupUserConverter.cs - - - JsonConverters\IdentifiableNameConverter.cs - - - JsonConverters\IssueCategoryConverter.cs - - - JsonConverters\IssueChildConverter.cs - - - JsonConverters\IssueConverter.cs - - - JsonConverters\IssueCustomFieldConverter.cs - - - JsonConverters\IssuePriorityConverter.cs - - - JsonConverters\IssueRelationConverter.cs - - - JsonConverters\IssueStatusConverter.cs - - - JsonConverters\JournalConverter.cs - - - JsonConverters\MembershipConverter.cs - - - JsonConverters\MembershipRoleConverter.cs - - - JsonConverters\NewsConverter.cs - - - JsonConverters\PermissionConverter.cs - - - JsonConverters\ProjectConverter.cs - - - JsonConverters\ProjectEnabledModuleConverter.cs - - - JsonConverters\ProjectIssueCategoryConverter.cs - - - JsonConverters\ProjectMembershipConverter.cs - - - JsonConverters\ProjectTrackerConverter.cs - - - JsonConverters\QueryConverter.cs - - - JsonConverters\RoleConverter.cs - - - JsonConverters\TimeEntryActivityConverter.cs - - - JsonConverters\TimeEntryConverter.cs - - - JsonConverters\TrackerConverter.cs - - - JsonConverters\TrackerCustomFieldConverter.cs - - - JsonConverters\UploadConverter.cs - - - JsonConverters\UserConverter.cs - - - JsonConverters\UserGroupConverter.cs - - - JsonConverters\VersionConverter.cs - - - JsonConverters\WatcherConverter.cs - - - JsonConverters\WikiPageConverter.cs - - - MimeFormat.cs - - - - - Exceptions\NotFoundException.cs - - - Exceptions\RedmineException.cs - - - Exceptions\RedmineTimeoutException.cs - - - Exceptions\NameResolutionFailureException.cs - - - Exceptions\InternalServerErrorException.cs - - - Exceptions\UnauthorizedException.cs - - - Exceptions\ForbiddenException.cs - - - Exceptions\ConflictException.cs - - - Exceptions\NotAcceptableException.cs - - - - - - - - - \ No newline at end of file diff --git a/redmine-net40-api/Async/RedmineManagerAsync.cs b/redmine-net40-api/Async/RedmineManagerAsync.cs deleted file mode 100644 index 12ef40a6..00000000 --- a/redmine-net40-api/Async/RedmineManagerAsync.cs +++ /dev/null @@ -1,249 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Threading.Tasks; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.Async -{ - /// - /// - /// - public static class RedmineManagerAsync - { - /// - /// Gets the current user asynchronous. - /// - /// The redmine manager. - /// The parameters. - /// - public static Task GetCurrentUserAsync(this RedmineManager redmineManager, NameValueCollection parameters = null) - { - return Task.Factory.StartNew(() => redmineManager.GetCurrentUser(parameters), TaskCreationOptions.LongRunning); - } - - /// - /// Creates the or update wiki page asynchronous. - /// - /// The redmine manager. - /// The project identifier. - /// Name of the page. - /// The wiki page. - /// - public static Task CreateOrUpdateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage) - { - return Task.Factory.StartNew(() => redmineManager.CreateOrUpdateWikiPage(projectId, pageName, wikiPage), TaskCreationOptions.LongRunning); - } - - /// - /// Deletes the wiki page asynchronous. - /// - /// The redmine manager. - /// The project identifier. - /// Name of the page. - /// - public static Task DeleteWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName) - { - return Task.Factory.StartNew(() => redmineManager.DeleteWikiPage(projectId, pageName), TaskCreationOptions.LongRunning); - } - - /// - /// Gets the wiki page asynchronous. - /// - /// The redmine manager. - /// The project identifier. - /// The parameters. - /// Name of the page. - /// The version. - /// - public static Task GetWikiPageAsync(this RedmineManager redmineManager, string projectId, NameValueCollection parameters, string pageName, uint version = 0) - { - return Task.Factory.StartNew(() => redmineManager.GetWikiPage(projectId, parameters, pageName, version), TaskCreationOptions.LongRunning); - } - - /// - /// Gets all wiki pages asynchronous. - /// - /// The redmine manager. - /// The project identifier. - /// - public static Task> GetAllWikiPagesAsync(this RedmineManager redmineManager, string projectId) - { - return Task.Factory.StartNew(() => redmineManager.GetAllWikiPages(projectId), TaskCreationOptions.LongRunning); - } - - /// - /// Adds the user to group asynchronous. - /// - /// The redmine manager. - /// The group identifier. - /// The user identifier. - /// - public static Task AddUserToGroupAsync(this RedmineManager redmineManager, int groupId, int userId) - { - return Task.Factory.StartNew(() => redmineManager.AddUserToGroup(groupId, userId), TaskCreationOptions.LongRunning); - } - - /// - /// Removes the user from group asynchronous. - /// - /// The redmine manager. - /// The group identifier. - /// The user identifier. - /// - public static Task RemoveUserFromGroupAsync(this RedmineManager redmineManager, int groupId, int userId) - { - return Task.Factory.StartNew(() => redmineManager.RemoveUserFromGroup(groupId, userId), TaskCreationOptions.LongRunning); - } - - /// - /// Adds the watcher to issue asynchronous. - /// - /// The redmine manager. - /// The issue identifier. - /// The user identifier. - /// - public static Task AddWatcherToIssueAsync(this RedmineManager redmineManager, int issueId, int userId) - { - return Task.Factory.StartNew(() => redmineManager.AddWatcherToIssue(issueId, userId), TaskCreationOptions.LongRunning); - } - - /// - /// Removes the watcher from issue asynchronous. - /// - /// The redmine manager. - /// The issue identifier. - /// The user identifier. - /// - public static Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManager, int issueId, int userId) - { - return Task.Factory.StartNew(() => redmineManager.RemoveWatcherFromIssue(issueId, userId), TaskCreationOptions.LongRunning); - } - - /// - /// Gets the object asynchronous. - /// - /// - /// The redmine manager. - /// The identifier. - /// The parameters. - /// - public static Task GetObjectAsync(this RedmineManager redmineManager, string id, NameValueCollection parameters) where T : class, new() - { - return Task.Factory.StartNew(() => redmineManager.GetObject(id, parameters), TaskCreationOptions.LongRunning); - } - - /// - /// Creates the object asynchronous. - /// - /// - /// The redmine manager. - /// The object. - /// - public static Task CreateObjectAsync(this RedmineManager redmineManager, T obj) where T : class, new() - { - return CreateObjectAsync(redmineManager, obj, null); - } - - /// - /// Creates the object asynchronous. - /// - /// - /// The redmine manager. - /// The object. - /// The owner identifier. - /// - public static Task CreateObjectAsync(this RedmineManager redmineManager, T obj, string ownerId) where T : class, new() - { - return Task.Factory.StartNew(() => redmineManager.CreateObject(obj, ownerId), TaskCreationOptions.LongRunning); - } - - /// - /// Gets the paginated objects asynchronous. - /// - /// - /// The redmine manager. - /// The parameters. - /// - public static Task> GetPaginatedObjectsAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new() - { - return Task.Factory.StartNew(() => redmineManager.GetPaginatedObjects(parameters), TaskCreationOptions.LongRunning); - } - - /// - /// Gets the objects asynchronous. - /// - /// - /// The redmine manager. - /// The parameters. - /// - public static Task> GetObjectsAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new() - { - return Task.Factory.StartNew(() => redmineManager.GetObjects(parameters), TaskCreationOptions.LongRunning); - } - - /// - /// Updates the object asynchronous. - /// - /// - /// The redmine manager. - /// The identifier. - /// The object. - /// The project identifier. - /// - public static Task UpdateObjectAsync(this RedmineManager redmineManager, string id, T obj, string projectId = null) where T : class, new() - { - return Task.Factory.StartNew(() => redmineManager.UpdateObject(id, obj, projectId), TaskCreationOptions.LongRunning); - } - - /// - /// Deletes the object asynchronous. - /// - /// - /// The redmine manager. - /// The identifier. - /// The parameters. - /// - public static Task DeleteObjectAsync(this RedmineManager redmineManager, string id, NameValueCollection parameters) where T : class, new() - { - return Task.Factory.StartNew(() => redmineManager.DeleteObject(id), TaskCreationOptions.LongRunning); - } - - /// - /// Uploads the file asynchronous. - /// - /// The redmine manager. - /// The data. - /// - public static Task UploadFileAsync(this RedmineManager redmineManager, byte[] data) - { - return Task.Factory.StartNew(() => redmineManager.UploadFile(data), TaskCreationOptions.LongRunning); - } - - /// - /// Downloads the file asynchronous. - /// - /// The redmine manager. - /// The address. - /// - public static Task DownloadFileAsync(this RedmineManager redmineManager, string address) - { - return Task.Factory.StartNew(() => redmineManager.DownloadFile(address), TaskCreationOptions.LongRunning); - } - } -} \ No newline at end of file diff --git a/redmine-net40-api/Async/RedmineManagerAsyncExtensions.cs b/redmine-net40-api/Async/RedmineManagerAsyncExtensions.cs deleted file mode 100755 index 30fb842f..00000000 --- a/redmine-net40-api/Async/RedmineManagerAsyncExtensions.cs +++ /dev/null @@ -1,324 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2016 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; -using System.Net; -using System.Text; -using System.Threading.Tasks; -using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.Extensions -{ - public static class RedmineManagerAsyncExtensions - { - public static Task GetCurrentUserAsync(this RedmineManager redmineManager, NameValueCollection parameters = null) - { - var task = Task.Factory.StartNew(() => - { - var uri = UrlHelper.GetCurrentUserUrl(redmineManager); - - using (var wc = redmineManager.CreateWebClient(parameters)) - { - return wc.DownloadString(uri); - } - }); - - return task.ContinueWith(t => RedmineSerializer.Deserialize(t.Result, redmineManager.MimeFormat)); - } - - public static Task CreateOrUpdateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage) - { - var task = Task.Factory.StartNew(() => - { - var uri = UrlHelper.GetWikiCreateOrUpdaterUrl(redmineManager, projectId, pageName); - var data = RedmineSerializer.Serialize(wikiPage,redmineManager.MimeFormat); - - using (var wc = redmineManager.CreateWebClient(null)) - { - var response = wc.UploadString(uri, RedmineManager.PUT, data); - return RedmineSerializer.Deserialize(response, redmineManager.MimeFormat); - } - }, TaskCreationOptions.LongRunning); - - return task; - } - - public static Task DeleteWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName) - { - var uri = UrlHelper.GetDeleteWikirUrl(redmineManager, projectId, pageName); - return Task.Factory.StartNew(() => - { - using (var wc = redmineManager.CreateWebClient(null)) - { - wc.UploadString(uri, RedmineManager.DELETE, string.Empty); - } - }, TaskCreationOptions.LongRunning); - } - - public static Task GetWikiPageAsync(this RedmineManager redmineManager, string projectId, NameValueCollection parameters, string pageName, uint version = 0) - { - var task = Task.Factory.StartNew(() => - { - var uri = UrlHelper.GetWikiPageUrl(redmineManager, projectId, parameters, pageName, version); - using (var wc = redmineManager.CreateWebClient(parameters)) - { - try - { - var response = wc.DownloadString(uri); - return RedmineSerializer.Deserialize(response, redmineManager.MimeFormat); - } - catch (WebException wex) - { - wex.HandleWebException("GetWikiPageAsync", redmineManager.MimeFormat); - } - return null; - } - }, TaskCreationOptions.LongRunning); - return task; - } - - public static Task> GetAllWikiPagesAsync(this RedmineManager redmineManager, NameValueCollection parameters, string projectId) - { - var task = Task.Factory.StartNew(() => - { - var uri = UrlHelper.GetWikisUrl(redmineManager, projectId); - using (var wc = redmineManager.CreateWebClient(parameters)) - { - var response = wc.DownloadString(uri); - return RedmineSerializer.DeserializeList(response, redmineManager.MimeFormat); - } - }, TaskCreationOptions.LongRunning); - return task; - } - - public static Task AddUserToGroupAsync(this RedmineManager redmineManager, int groupId, int userId) - { - var data = redmineManager.MimeFormat == MimeFormat.xml - ? "" + userId + "" - : "{\"user_id\":\"" + userId + "\"}"; - var task = Task.Factory.StartNew(() => - { - var uri = UrlHelper.GetAddUserToGroupUrl(redmineManager, groupId); - using (var wc = redmineManager.CreateWebClient(null)) - { - wc.UploadString(uri, RedmineManager.POST, data); - } - }, TaskCreationOptions.LongRunning); - return task; - } - - public static Task RemoveUserFromGroupAsync(this RedmineManager redmineManager, int groupId, int userId) - { - var task = Task.Factory.StartNew(() => - { - var uri = UrlHelper.GetRemoveUserFromGroupUrl(redmineManager, groupId, userId); - using (var wc = redmineManager.CreateWebClient(null)) - { - wc.UploadString(uri, RedmineManager.DELETE, string.Empty); - } - }, TaskCreationOptions.LongRunning); - return task; - } - - public static Task AddWatcherToIssueAsync(this RedmineManager redmineManager, int issueId, int userId) - { - var data = redmineManager.MimeFormat == MimeFormat.xml - ? "" + userId + "" - : "{\"user_id\":\"" + userId + "\"}"; - var task = Task.Factory.StartNew(() => - { - var uri = UrlHelper.GetAddWatcherUrl(redmineManager, issueId, userId); - - using (var wc = redmineManager.CreateWebClient(null)) - { - wc.UploadString(uri, RedmineManager.POST, data); - } - }, TaskCreationOptions.LongRunning); - return task; - } - - public static Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManager, int issueId, int userId) - { - var task = Task.Factory.StartNew(() => - { - var uri = UrlHelper.GetRemoveWatcherUrl(redmineManager, issueId, userId); - using (var wc = redmineManager.CreateWebClient(null)) - { - wc.UploadString(uri, RedmineManager.DELETE, string.Empty); - } - }, TaskCreationOptions.LongRunning); - return task; - } - - public static Task GetObjectAsync(this RedmineManager redmineManager, string id, NameValueCollection parameters) where T : class, new() - { - var task = Task.Factory.StartNew(() => - { - var url = UrlHelper.GetGetUrl(redmineManager, id); - using (var wc = redmineManager.CreateWebClient(parameters)) - { - try - { - var response = wc.DownloadString(url); - return RedmineSerializer.Deserialize(response, redmineManager.MimeFormat); - } - catch (WebException wex) - { - wex.HandleWebException("GetObject", redmineManager.MimeFormat); - } - return null; - } - }, TaskCreationOptions.LongRunning); - return task; - } - - public static Task CreateObjectAsync(this RedmineManager redmineManager, T obj) where T : class, new() - { - return CreateObjectAsync(redmineManager, obj, null); - } - - public static Task CreateObjectAsync(this RedmineManager redmineManager, T obj, string ownerId) where T : class, new() - { - var task = Task.Factory.StartNew(() => - { - var url = UrlHelper.GetCreateUrl(redmineManager, ownerId); - var data = RedmineSerializer.Serialize(obj,redmineManager.MimeFormat); - - using (var wc = redmineManager.CreateWebClient(null)) - { - var response = wc.UploadString(url, RedmineManager.POST, data); - return RedmineSerializer.Deserialize(response, redmineManager.MimeFormat); - } - }, TaskCreationOptions.LongRunning); - return task; - } - - public static Task> GetPaginatedObjectsAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new() - { - var task = Task.Factory.StartNew(() => - { - var url = UrlHelper.GetListUrl(redmineManager, parameters); - using (var wc = redmineManager.CreateWebClient(parameters)) - { - var response = wc.DownloadString(url); - return RedmineSerializer.DeserializeList(response, redmineManager.MimeFormat); - } - }, TaskCreationOptions.LongRunning); - return task; - } - - public static Task> GetObjectsAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new() - { - var task = Task.Factory.StartNew(() => - { - int totalCount = 0, pageSize; - List resultList = null; - if (parameters == null) parameters = new NameValueCollection(); - int offset = 0; - int.TryParse(parameters[RedmineKeys.LIMIT], out pageSize); - if (pageSize == default(int)) - { - pageSize = redmineManager.PageSize > 0 ? redmineManager.PageSize : 25; - parameters.Set(RedmineKeys.LIMIT, pageSize.ToString(CultureInfo.InvariantCulture)); - } - do - { - parameters.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); - var requestTask = redmineManager.GetPaginatedObjectsAsync(parameters).ContinueWith(t => - { - if (t.Result != null) - { - if (resultList == null) - { - resultList = t.Result.Objects; - totalCount = t.Result.TotalCount; - } - else - resultList.AddRange(t.Result.Objects); - } - offset += pageSize; - }); - requestTask.Wait(TimeSpan.FromMilliseconds(5000)); - } while (offset < totalCount); - return resultList; - }); - return task; - } - - public static Task UpdateObjectAsync(this RedmineManager redmineManager, string id, T obj, string projectId = null) where T : class, new() - { - var task = Task.Factory.StartNew(() => - { - var url = UrlHelper.GetUploadUrl(redmineManager, id, obj, projectId); - using (var wc = redmineManager.CreateWebClient(null)) - { - var data = RedmineSerializer.Serialize(obj,redmineManager.MimeFormat); - wc.UploadString(url, RedmineManager.PUT, data); - } - }, TaskCreationOptions.LongRunning); - return task; - } - - public static Task DeleteObjectAsync(this RedmineManager redmineManager, string id, NameValueCollection parameters) where T : class, new() - { - var task = Task.Factory.StartNew(() => - { - var uri = UrlHelper.GetDeleteUrl(redmineManager, id); - - using (var wc = redmineManager.CreateWebClient(parameters)) - { - wc.UploadString(uri, RedmineManager.DELETE, string.Empty); - } - }, TaskCreationOptions.LongRunning); - return task; - } - - public static Task UploadFileAsync(this RedmineManager redmineManager, byte[] data) - { - var task = Task.Factory.StartNew(() => - { - var uri = UrlHelper.GetUploadFileUrl(redmineManager); - using (var wc = redmineManager.CreateWebClient(null, true)) - { - var response = wc.UploadData(uri, RedmineManager.POST, data); - - var responseString = Encoding.ASCII.GetString(response); - return RedmineSerializer.Deserialize(responseString, redmineManager.MimeFormat); - } - }, TaskCreationOptions.LongRunning); - - return task; - } - - public static Task DownloadFileAsync(this RedmineManager redmineManager, string address) - { - var task = Task.Factory.StartNew(() => - { - using (var wc = redmineManager.CreateWebClient(null)) - { - wc.Headers.Add(HttpRequestHeader.Accept, "application/octet-stream"); - var response = wc.DownloadData(address); - return response; - } - }, TaskCreationOptions.LongRunning); - return task; - } - } -} \ No newline at end of file diff --git a/redmine-net40-api/Extensions/CollectionExtensions.cs b/redmine-net40-api/Extensions/CollectionExtensions.cs deleted file mode 100755 index 87c135fe..00000000 --- a/redmine-net40-api/Extensions/CollectionExtensions.cs +++ /dev/null @@ -1,62 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; - -namespace Redmine.Net.Api.Extensions -{ - /// - /// - /// - - - public static class CollectionExtensions - { - /// - /// Clones the specified list to clone. - /// - /// - /// The list to clone. - /// - public static IList Clone(this IList listToClone) where T : ICloneable - { - if (listToClone == null) return null; - IList clonedList = new List(); - foreach (var item in listToClone) - clonedList.Add((T) item.Clone()); - return clonedList; - } - - - /// - /// Equalses the specified list to compare. - /// - /// - /// The list. - /// The list to compare. - /// - public static bool Equals(this IList list, IList listToCompare) where T : class - { - if (listToCompare == null) return false; - - var set = new HashSet(list); - var setToCompare = new HashSet(listToCompare); - - return set.SetEquals(setToCompare); - } - } -} \ No newline at end of file diff --git a/redmine-net40-api/Extensions/JObjectExtensions.cs b/redmine-net40-api/Extensions/JObjectExtensions.cs deleted file mode 100755 index 729bf8eb..00000000 --- a/redmine-net40-api/Extensions/JObjectExtensions.cs +++ /dev/null @@ -1,59 +0,0 @@ -ο»Ώusing System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Web.Script.Serialization; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Redmine.Net.Api.JSonConverters; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.Extensions -{ - public static class JObjectExtensions - { - public static IdentifiableName GetValueAsIdentifiableName(this JObject obj, string key) - { - JToken val; - - if (!obj.TryGetValue(key, out val)) return null; - - //var ser = new JavaScriptSerializer(); - //ser.RegisterConverters(new[] { new IdentifiableNameConverter() }); - - //var result = ser.ConvertToType(val); - //return result; - - return val.ToObject(); - } - - public static List GetValueAsCollection(this JObject obj, string key) where T : new() - { - JToken val; - - if (!obj.TryGetValue(key, out val)) return null; - - //var ser = new JavaScriptSerializer(); - //ser.RegisterConverters(new[] { RedmineSerializer.JsonConverters[typeof(T)] }); - - //var list = new List(); - - //var arrayList = val as ArrayList; - //if (arrayList != null) - //{ - // list.AddRange(from object item in arrayList select ser.ConvertToType(item)); - //} - //else - //{ - // var dict = val as Dictionary; - // if (dict != null) - // { - // list.AddRange(dict.Select(pair => ser.ConvertToType(pair.Value))); - // } - //} - //return list; - - return null; - } - } -} diff --git a/redmine-net40-api/Extensions/JsonExtensions.cs b/redmine-net40-api/Extensions/JsonExtensions.cs deleted file mode 100755 index 635f777b..00000000 --- a/redmine-net40-api/Extensions/JsonExtensions.cs +++ /dev/null @@ -1,223 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Internals; -using Redmine.Net.Api.JSonConverters; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.Extensions -{ - /// - /// - /// - public static class JsonExtensions - { - /// - /// Writes the identifier if not null. - /// - /// The dictionary. - /// The ident. - /// The key. - public static void WriteIdIfNotNull(this Dictionary dictionary, IdentifiableName ident, string key) - { - if (ident != null) dictionary.Add(key, ident.Id); - } - - /// - /// Writes the identifier or empty. - /// - /// The dictionary. - /// The ident. - /// The key. - /// The empty value. - public static void WriteIdOrEmpty(this Dictionary dictionary, IdentifiableName ident, string key, string emptyValue = null) - { - if (ident != null) dictionary.Add(key, ident.Id); - else dictionary.Add(key, emptyValue); - } - - /// - /// Writes the array. - /// - /// - /// The dictionary. - /// The key. - /// The col. - /// The converter. - /// The serializer. - public static void WriteArray(this Dictionary dictionary, string key, IEnumerable col, - JavaScriptConverter converter, JavaScriptSerializer serializer) - { - if (col != null) - { - serializer.RegisterConverters(new[] { converter }); - dictionary.Add(key, col.ToArray()); - } - } - - /// - /// Writes the ids array. - /// - /// The dictionary. - /// The key. - /// The coll. - public static void WriteIdsArray(this Dictionary dictionary, string key, - IEnumerable coll) - { - if (coll != null) - dictionary.Add(key, coll.Select(x => x.Id).ToArray()); - } - - /// - /// Writes the names array. - /// - /// The dictionary. - /// The key. - /// The coll. - public static void WriteNamesArray(this Dictionary dictionary, string key, - IEnumerable coll) - { - if (coll != null) - dictionary.Add(key, coll.Select(x => x.Name).ToArray()); - } - - /// - /// Writes the date or empty. - /// - /// The dictionary. - /// The value. - /// The tag. - public static void WriteDateOrEmpty(this Dictionary dictionary, DateTime? val, string tag) - { - if (!val.HasValue || val.Value.Equals(default(DateTime))) - dictionary.Add(tag, string.Empty); - else - dictionary.Add(tag, string.Format(NumberFormatInfo.InvariantInfo, "{0}", val.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture))); - } - - /// - /// Writes the value or empty. - /// - /// - /// The dictionary. - /// The value. - /// The tag. - public static void WriteValueOrEmpty(this Dictionary dictionary, T? val, string tag) where T : struct - { - if (!val.HasValue || EqualityComparer.Default.Equals(val.Value, default(T))) - dictionary.Add(tag, string.Empty); - else - dictionary.Add(tag, val.Value); - } - - /// - /// Writes the value or default. - /// - /// - /// The dictionary. - /// The value. - /// The tag. - public static void WriteValueOrDefault(this Dictionary dictionary, T? val, string tag) where T : struct - { - dictionary.Add(tag, val ?? default(T)); - } - - /// - /// Gets the value. - /// - /// - /// The dictionary. - /// The key. - /// - public static T GetValue(this IDictionary dictionary, string key) - { - object val; - var dict = dictionary; - var type = typeof(T); - if (!dict.TryGetValue(key, out val)) return default(T); - - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - if (val == null) return default(T); - - type = Nullable.GetUnderlyingType(type); - } - - if (val.GetType() == typeof(ArrayList)) return (T)val; - - if (type.IsEnum) val = Enum.Parse(type, val.ToString(), true); - - return (T)Convert.ChangeType(val, type, CultureInfo.InvariantCulture); - } - - /// - /// Gets the name of the value as identifiable. - /// - /// The dictionary. - /// The key. - /// - public static IdentifiableName GetValueAsIdentifiableName(this IDictionary dictionary, string key) - { - object val; - if (!dictionary.TryGetValue(key, out val)) return null; - - var ser = new JavaScriptSerializer(); - ser.RegisterConverters(new[] { new IdentifiableNameConverter() }); - - var result = ser.ConvertToType(val); - return result; - } - - /// - /// For Json - /// - /// - /// The dictionary. - /// The key. - /// - public static List GetValueAsCollection(this IDictionary dictionary, string key) where T : new() - { - object val; - if (!dictionary.TryGetValue(key, out val)) return null; - - var ser = new JavaScriptSerializer(); - ser.RegisterConverters(new[] { RedmineSerializer.JsonConverters[typeof(T)] }); - - var list = new List(); - - var arrayList = val as ArrayList; - if (arrayList != null) - { - list.AddRange(from object item in arrayList select ser.ConvertToType(item)); - } - else - { - var dict = val as Dictionary; - if (dict != null) - { - list.AddRange(dict.Select(pair => ser.ConvertToType(pair.Value))); - } - } - return list; - } - } -} \ No newline at end of file diff --git a/redmine-net40-api/Extensions/WebExtensions.cs b/redmine-net40-api/Extensions/WebExtensions.cs deleted file mode 100644 index 3e40a146..00000000 --- a/redmine-net40-api/Extensions/WebExtensions.cs +++ /dev/null @@ -1,121 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Exceptions; - -namespace Redmine.Net.Api.Extensions -{ - /// - /// - /// - public static class WebExtensions - { - /// - /// Handles the web exception. - /// - /// The exception. - /// The method. - /// The MIME format. - /// Timeout! - /// Bad domain name! - /// - /// - /// - /// - /// The page that you are trying to update is staled! - /// - /// - /// - public static void HandleWebException(this WebException exception, string method, MimeFormat mimeFormat) - { - if (exception == null) return; - - switch (exception.Status) - { - case WebExceptionStatus.Timeout: throw new RedmineTimeoutException("Timeout!", exception); - case WebExceptionStatus.NameResolutionFailure: throw new NameResolutionFailureException("Bad domain name!", exception); - case WebExceptionStatus.ProtocolError: - { - var response = (HttpWebResponse)exception.Response; - switch ((int)response.StatusCode) - { - - case (int)HttpStatusCode.NotFound: - throw new NotFoundException (response.StatusDescription, exception); - - case (int)HttpStatusCode.InternalServerError: - throw new InternalServerErrorException(response.StatusDescription, exception); - - case (int)HttpStatusCode.Unauthorized: - throw new UnauthorizedException(response.StatusDescription, exception); - - case (int)HttpStatusCode.Forbidden: - throw new ForbiddenException(response.StatusDescription, exception); - - case (int)HttpStatusCode.Conflict: - throw new ConflictException("The page that you are trying to update is staled!", exception); - - case 422: - - var errors = GetRedmineExceptions(exception.Response, mimeFormat); - string message = string.Empty; - if (errors != null) - { - message = errors.Aggregate(message, (current, error) => current + (error.Info + "\n")); - } - throw new RedmineException(method + " has invalid or missing attribute parameters: " + message, exception); - - case (int)HttpStatusCode.NotAcceptable: throw new NotAcceptableException(response.StatusDescription, exception); - } - } - break; - - default: throw new RedmineException(exception.Message, exception); - } - } - - /// - /// Gets the redmine exceptions. - /// - /// The web response. - /// The MIME format. - /// - private static List GetRedmineExceptions(this WebResponse webResponse, MimeFormat mimeFormat) - { - using (var dataStream = webResponse.GetResponseStream()) - { - if (dataStream == null) return null; - using (var reader = new StreamReader(dataStream)) - { - var responseFromServer = reader.ReadToEnd(); - - if (responseFromServer.Trim().Length > 0) - { - var errors = RedmineSerializer.DeserializeList(responseFromServer, mimeFormat); - return errors.Objects; - } - } - return null; - } - } - } -} \ No newline at end of file diff --git a/redmine-net40-api/Extensions/XmlReaderExtensions.cs b/redmine-net40-api/Extensions/XmlReaderExtensions.cs deleted file mode 100755 index f772e752..00000000 --- a/redmine-net40-api/Extensions/XmlReaderExtensions.cs +++ /dev/null @@ -1,219 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Xml; -using System.Xml.Serialization; - -namespace Redmine.Net.Api.Extensions -{ - /// - /// - public static class XmlReaderExtensions - { - /// - /// Reads the attribute as int. - /// - /// The reader. - /// Name of the attribute. - /// - public static int ReadAttributeAsInt(this XmlReader reader, string attributeName) - { - var attribute = reader.GetAttribute(attributeName); - int result; - if (string.IsNullOrWhiteSpace(attribute) || - !int.TryParse(attribute, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out result)) - return default(int); - return result; - } - - /// - /// Reads the attribute as nullable int. - /// - /// The reader. - /// Name of the attribute. - /// - public static int? ReadAttributeAsNullableInt(this XmlReader reader, string attributeName) - { - var attribute = reader.GetAttribute(attributeName); - int result; - if (string.IsNullOrWhiteSpace(attribute) || - !int.TryParse(attribute, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out result)) return null; - return result; - } - - /// - /// Reads the attribute as boolean. - /// - /// The reader. - /// Name of the attribute. - /// - public static bool ReadAttributeAsBoolean(this XmlReader reader, string attributeName) - { - var attribute = reader.GetAttribute(attributeName); - bool result; - if (string.IsNullOrWhiteSpace(attribute) || !bool.TryParse(attribute, out result)) return false; - - return result; - } - - /// - /// Reads the element content as nullable date time. - /// - /// The reader. - /// - public static DateTime? ReadElementContentAsNullableDateTime(this XmlReader reader) - { - var str = reader.ReadElementContentAsString(); - - // Format for journals, attachments etc. - var format = "yyyy'-'MM'-'dd HH':'mm':'ss UTC"; - - DateTime result; - if (string.IsNullOrWhiteSpace(str) || !DateTime.TryParse(str, out result)) - { - if (!DateTime.TryParseExact(str, format, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) - return null; - } - return result; - } - - /// - /// Reads the element content as nullable float. - /// - /// The reader. - /// - public static float? ReadElementContentAsNullableFloat(this XmlReader reader) - { - var str = reader.ReadElementContentAsString(); - - float result; - if (string.IsNullOrWhiteSpace(str) || - !float.TryParse(str, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out result)) return null; - - return result; - } - - /// - /// Reads the element content as nullable int. - /// - /// The reader. - /// - public static int? ReadElementContentAsNullableInt(this XmlReader reader) - { - var str = reader.ReadElementContentAsString(); - - int result; - if (string.IsNullOrWhiteSpace(str) || - !int.TryParse(str, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out result)) return null; - - return result; - } - - /// - /// Reads the element content as nullable decimal. - /// - /// The reader. - /// - public static decimal? ReadElementContentAsNullableDecimal(this XmlReader reader) - { - var str = reader.ReadElementContentAsString(); - - decimal result; - if (string.IsNullOrWhiteSpace(str) || - !decimal.TryParse(str, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out result)) return null; - - return result; - } - - /// - /// Reads the element content as collection. - /// - /// - /// The reader. - /// - public static List ReadElementContentAsCollection(this XmlReader reader) where T : class - { - var result = new List(); - var serializer = new XmlSerializer(typeof(T)); - var xml = reader.ReadOuterXml(); - using (var sr = new StringReader(xml)) - { - var r = new XmlTextReader(sr); - r.ReadStartElement(); - while (!r.EOF) - { - if (r.NodeType == XmlNodeType.EndElement) - { - r.ReadEndElement(); - continue; - } - - T temp; - - if (r.IsEmptyElement && r.HasAttributes) - { - temp = serializer.Deserialize(r) as T; - } - else - { - var subTree = r.ReadSubtree(); - temp = serializer.Deserialize(subTree) as T; - } - if (temp != null) result.Add(temp); - if (!r.IsEmptyElement) r.Read(); - } - } - return result; - } - - /// - /// Reads the element content as collection. - /// - /// The reader. - /// The type. - /// - public static ArrayList ReadElementContentAsCollection(this XmlReader reader, Type type) - { - var result = new ArrayList(); - var serializer = new XmlSerializer(type); - var xml = reader.ReadOuterXml(); - using (var sr = new StringReader(xml)) - { - var r = new XmlTextReader(sr); - r.ReadStartElement(); - while (!r.EOF) - { - if (r.NodeType == XmlNodeType.EndElement) - { - r.ReadEndElement(); - continue; - } - - var subTree = r.ReadSubtree(); - var temp = serializer.Deserialize(subTree); - if (temp != null) result.Add(temp); - r.Read(); - } - } - return result; - } - } -} \ No newline at end of file diff --git a/redmine-net40-api/Internals/RedmineSerializer.cs b/redmine-net40-api/Internals/RedmineSerializer.cs deleted file mode 100755 index 24591449..00000000 --- a/redmine-net40-api/Internals/RedmineSerializer.cs +++ /dev/null @@ -1,230 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.IO; -using System.Linq; -using System.Xml; -using System.Xml.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Exceptions; - -namespace Redmine.Net.Api.Internals -{ - internal static partial class RedmineSerializer - { - /// - /// Serializes the specified System.Object and writes the XML document to a string. - /// - /// The type of objects to serialize. - /// The object to serialize. - /// - /// The System.String that contains the XML document. - /// - /// - // ReSharper disable once InconsistentNaming - private static string ToXML(T obj) where T : class - { - var xws = new XmlWriterSettings {OmitXmlDeclaration = true}; - using (var stringWriter = new StringWriter()) - { - using (var xmlWriter = XmlWriter.Create(stringWriter, xws)) - { - var sr = new XmlSerializer(typeof (T)); - sr.Serialize(xmlWriter, obj); - return stringWriter.ToString(); - } - } - } - - /// - /// Deserializes the XML document contained by the specific System.String. - /// - /// The type of objects to deserialize. - /// The System.String that contains the XML document to deserialize. - /// - /// The T object being deserialized. - /// - /// An error occurred during deserialization. The original exception is available - /// using the System.Exception.InnerException property. - // ReSharper disable once InconsistentNaming - private static T FromXML(string xml) where T : class - { - using (var text = new StringReader(xml)) - { - var sr = new XmlSerializer(typeof (T)); - return sr.Deserialize(text) as T; - } - } - - /// - /// Serializes the specified type T and writes the XML document to a string. - /// - /// - /// The object. - /// The MIME format. - /// - /// Serialization error - public static string Serialize(T obj, MimeFormat mimeFormat) where T : class, new() - { - try - { - if (mimeFormat == MimeFormat.Json) - { - return JsonSerializer(obj); - } - return ToXML(obj); - } - catch (Exception ex) - { - throw new RedmineException("Serialization error", ex); - } - } - - /// - /// Deserializes the XML document contained by the specific System.String. - /// - /// - /// The response. - /// The MIME format. - /// - /// - /// Could not deserialize null! - /// or - /// Deserialization error - /// - /// - /// - /// - public static T Deserialize(string response, MimeFormat mimeFormat) where T : class, new() - { - if (string.IsNullOrEmpty(response)) throw new RedmineException("Could not deserialize null!"); - try - { - if (mimeFormat == MimeFormat.Json) - { - var type = typeof (T); - var jsonRoot = (string) null; - if (type == typeof (IssueCategory)) jsonRoot = RedmineKeys.ISSUE_CATEGORY; - if (type == typeof (IssueRelation)) jsonRoot = RedmineKeys.RELATION; - if (type == typeof (TimeEntry)) jsonRoot = RedmineKeys.TIME_ENTRY; - if (type == typeof (ProjectMembership)) jsonRoot = RedmineKeys.MEMBERSHIP; - if (type == typeof (WikiPage)) jsonRoot = RedmineKeys.WIKI_PAGE; - return JsonDeserialize(response, jsonRoot); - } - - return FromXML(response); - } - catch (Exception ex) - { - throw new RedmineException("Deserialization error",ex); - } - } - - /// - /// Deserializes the list. - /// - /// - /// The response. - /// The MIME format. - /// - /// - /// Could not deserialize null! - /// or - /// Deserialization error - /// - public static PaginatedObjects DeserializeList(string response, MimeFormat mimeFormat) - where T : class, new() - { - try - { - if (string.IsNullOrWhiteSpace(response)) throw new RedmineException("Could not deserialize null!"); - - if (mimeFormat == MimeFormat.Json) - { - return JSonDeserializeList(response); - } - - return XmlDeserializeList(response); - } - - catch (Exception ex) - { - throw new RedmineException("Deserialization error", ex); - } - } - - /// - /// js the son deserialize list. - /// - /// - /// The response. - /// - private static PaginatedObjects JSonDeserializeList(string response) where T : class, new() - { - - int totalItems, offset; - var type = typeof(T); - var jsonRoot = (string)null; - if (type == typeof(Error)) jsonRoot = RedmineKeys.ERRORS; - if (type == typeof(WikiPage)) jsonRoot = RedmineKeys.WIKI_PAGES; - if (type == typeof(IssuePriority)) jsonRoot = RedmineKeys.ISSUE_PRIORITIES; - if (type == typeof(TimeEntryActivity)) jsonRoot = RedmineKeys.TIME_ENTRY_ACTIVITIES; - - if (string.IsNullOrEmpty(jsonRoot)) - jsonRoot = RedmineManager.Sufixes[type]; - - var result = JsonDeserializeToList(response, jsonRoot, out totalItems, out offset); - - return new PaginatedObjects() - { - TotalCount = totalItems, - Offset = offset, - Objects = result.ToList() - }; - } - - /// - /// XMLs the deserialize list. - /// - /// - /// The response. - /// - private static PaginatedObjects XmlDeserializeList(string response) where T : class, new() - { - using (var stringReader = new StringReader(response)) - { - using (var xmlReader = new XmlTextReader(stringReader)) - { - xmlReader.WhitespaceHandling = WhitespaceHandling.None; - xmlReader.Read(); - xmlReader.Read(); - - var totalItems = xmlReader.ReadAttributeAsInt(RedmineKeys.TOTAL_COUNT); - var offset = xmlReader.ReadAttributeAsInt(RedmineKeys.OFFSET); - var result = xmlReader.ReadElementContentAsCollection(); - return new PaginatedObjects() - { - TotalCount = totalItems, - Offset = offset, - Objects = result - }; - } - } - } - } -} \ No newline at end of file diff --git a/redmine-net40-api/Internals/RedmineSerializerJson.cs b/redmine-net40-api/Internals/RedmineSerializerJson.cs deleted file mode 100755 index a01850f5..00000000 --- a/redmine-net40-api/Internals/RedmineSerializerJson.cs +++ /dev/null @@ -1,243 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using System.Collections; -using System.Linq; -using Redmine.Net.Api.JSonConverters; -using Redmine.Net.Api.Types; -using Version = Redmine.Net.Api.Types.Version; - -namespace Redmine.Net.Api.Internals -{ - /// - /// - /// - internal static partial class RedmineSerializer - { - private static readonly Dictionary jsonConverters = new Dictionary - { - {typeof (Issue), new IssueConverter()}, - {typeof (Project), new ProjectConverter()}, - {typeof (User), new UserConverter()}, - {typeof (UserGroup), new UserGroupConverter()}, - {typeof (News), new NewsConverter()}, - {typeof (Query), new QueryConverter()}, - {typeof (Version), new VersionConverter()}, - {typeof (Attachment), new AttachmentConverter()}, - {typeof (Attachments), new AttachmentsConverter()}, - {typeof (IssueRelation), new IssueRelationConverter()}, - {typeof (TimeEntry), new TimeEntryConverter()}, - {typeof (IssueStatus),new IssueStatusConverter()}, - {typeof (Tracker),new TrackerConverter()}, - {typeof (TrackerCustomField),new TrackerCustomFieldConverter()}, - {typeof (IssueCategory), new IssueCategoryConverter()}, - {typeof (Role), new RoleConverter()}, - {typeof (ProjectMembership), new ProjectMembershipConverter()}, - {typeof (Group), new GroupConverter()}, - {typeof (GroupUser), new GroupUserConverter()}, - {typeof (Error), new ErrorConverter()}, - {typeof (IssueCustomField), new IssueCustomFieldConverter()}, - {typeof (ProjectTracker), new ProjectTrackerConverter()}, - {typeof (Journal), new JournalConverter()}, - {typeof (TimeEntryActivity), new TimeEntryActivityConverter()}, - {typeof (IssuePriority), new IssuePriorityConverter()}, - {typeof (WikiPage), new WikiPageConverter()}, - {typeof (Detail), new DetailConverter()}, - {typeof (ChangeSet), new ChangeSetConverter()}, - {typeof (Membership), new MembershipConverter()}, - {typeof (MembershipRole), new MembershipRoleConverter()}, - {typeof (IdentifiableName), new IdentifiableNameConverter()}, - {typeof (Permission), new PermissionConverter()}, - {typeof (IssueChild), new IssueChildConverter()}, - {typeof (ProjectIssueCategory), new ProjectIssueCategoryConverter()}, - {typeof (Watcher), new WatcherConverter()}, - {typeof (Upload), new UploadConverter()}, - {typeof (ProjectEnabledModule), new ProjectEnabledModuleConverter()}, - {typeof (CustomField), new CustomFieldConverter()}, - {typeof (CustomFieldRole), new CustomFieldRoleConverter()}, - {typeof (CustomFieldPossibleValue), new CustomFieldPossibleValueConverter()}, - {typeof (File), new FileConverter() } - }; - - /// - /// Available json converters. - /// - public static Dictionary JsonConverters { get { return jsonConverters; } } - - /// - /// Jsons the serializer. - /// - /// - /// The type. - /// - public static string JsonSerializer(T type) where T : new() - { - var serializer = new JavaScriptSerializer() { MaxJsonLength = int.MaxValue }; - serializer.RegisterConverters(new[] { jsonConverters[typeof(T)] }); - return serializer.Serialize(type); - } - - /// - /// JSON Deserialization - /// - /// - /// The json string. - /// The root. - /// - public static List JsonDeserializeToList(string jsonString, string root) where T : class, new() - { - int totalCount; - int offset; - return JsonDeserializeToList(jsonString, root, out totalCount, out offset); - } - - /// - /// JSON Deserialization - /// - /// - /// The json string. - /// The root. - /// The total count. - /// The offset. - /// - public static List JsonDeserializeToList(string jsonString, string root, out int totalCount, out int offset) where T : class,new() - { - var result = JsonDeserializeToList(jsonString, root, typeof(T), out totalCount, out offset); - return ((ArrayList)result).OfType().ToList(); - } - - /// - /// Jsons the deserialize. - /// - /// - /// The json string. - /// The root. - /// - public static T JsonDeserialize(string jsonString, string root) where T : new() - { - var type = typeof(T); - var result = JsonDeserialize(jsonString, type, root); - return result == null ? default(T) : (T) result; - } - - /// - /// Jsons the deserialize. - /// - /// The json string. - /// The type. - /// The root. - /// - /// jsonString - /// - /// - /// - public static object JsonDeserialize(string jsonString, Type type, string root) - { - if (string.IsNullOrEmpty(jsonString)) throw new ArgumentNullException("jsonString"); - - var serializer = new JavaScriptSerializer(); - serializer.RegisterConverters(new[] { jsonConverters[type] }); - - var dictionary = serializer.Deserialize>(jsonString); - if (dictionary == null) return null; - - object obj; - return !dictionary.TryGetValue(root ?? type.Name.ToLowerInvariant(), out obj) ? null : serializer.ConvertToType(obj, type); - } - - /// - /// Adds to list. - /// - /// The serializer. - /// The list. - /// The type. - /// The array list. - private static void AddToList(JavaScriptSerializer serializer, IList list, Type type, object arrayList) - { - foreach (var obj in (ArrayList)arrayList) - { - if (obj is ArrayList) - { - AddToList(serializer, list, type, obj); - } - else - { - var convertedType = serializer.ConvertToType(obj, type); - list.Add(convertedType); - } - } - } - - /// - /// Jsons the deserialize to list. - /// - /// The json string. - /// The root. - /// The type. - /// The total count. - /// The offset. - /// - /// jsonString - private static object JsonDeserializeToList(string jsonString, string root, Type type, out int totalCount, out int offset) - { - totalCount = 0; - offset = 0; - if (string.IsNullOrEmpty(jsonString)) throw new ArgumentNullException("jsonString"); - - var serializer = new JavaScriptSerializer(); - serializer.RegisterConverters(new[] { jsonConverters[type] }); - var dictionary = serializer.Deserialize>(jsonString); - if (dictionary == null) return null; - - object obj, tc, off; - - if (dictionary.TryGetValue(RedmineKeys.TOTAL_COUNT, out tc)) totalCount = (int)tc; - - if (dictionary.TryGetValue(RedmineKeys.OFFSET, out off)) offset = (int)off; - - if (!dictionary.TryGetValue(root.ToLowerInvariant(), out obj)) return null; - - var arrayList = new ArrayList(); - if (type == typeof(Error)) - { - string info = null; - foreach (var item in (ArrayList)obj) - { - var innerArrayList = item as ArrayList; - if (innerArrayList != null) - { - info = innerArrayList.Cast() - .Aggregate(info, (current, item2) => current + (item2 as string + " ")); - } - else - { - info += item as string + " "; - } - } - var err = new Error { Info = info }; - arrayList.Add(err); - } - else - { - AddToList(serializer, arrayList, type, obj); - } - return arrayList; - } - } -} \ No newline at end of file diff --git a/redmine-net40-api/Internals/RedmineSerializerJson2.cs b/redmine-net40-api/Internals/RedmineSerializerJson2.cs deleted file mode 100755 index a0f197b5..00000000 --- a/redmine-net40-api/Internals/RedmineSerializerJson2.cs +++ /dev/null @@ -1,212 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2016 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Redmine.Net.Api.JSonConverters2; -using Redmine.Net.Api.Types; -using Version = Redmine.Net.Api.Types.Version; - -namespace Redmine.Net.Api.Internals -{ - internal static partial class RedmineSerializer - { - private static readonly Dictionary jsonConverters = new Dictionary - { - {typeof (Issue), new IssueConverter()}, - {typeof (UploadConverter), new UploadConverter()}, - {typeof (IssueCustomField), new IssueCustomFieldConverter()} - //{typeof (Issue), new IssueConverter()}, - //{typeof (Project), new ProjectConverter()}, - //{typeof (User), new UserConverter()}, - //{typeof (UserGroup), new UserGroupConverter()}, - //{typeof (News), new NewsConverter()}, - //{typeof (Query), new QueryConverter()}, - //{typeof (Version), new VersionConverter()}, - //{typeof (Attachment), new AttachmentConverter()}, - //{typeof (Attachments), new AttachmentsConverter()}, - //{typeof (IssueRelation), new IssueRelationConverter()}, - //{typeof (TimeEntry), new TimeEntryConverter()}, - //{typeof (IssueStatus),new IssueStatusConverter()}, - //{typeof (Tracker),new TrackerConverter()}, - //{typeof (TrackerCustomField),new TrackerCustomFieldConverter()}, - //{typeof (IssueCategory), new IssueCategoryConverter()}, - //{typeof (Role), new RoleConverter()}, - //{typeof (ProjectMembership), new ProjectMembershipConverter()}, - //{typeof (Group), new GroupConverter()}, - //{typeof (GroupUser), new GroupUserConverter()}, - //{typeof (Error), new ErrorConverter()}, - //{typeof (IssueCustomField), new IssueCustomFieldConverter()}, - //{typeof (ProjectTracker), new ProjectTrackerConverter()}, - //{typeof (Journal), new JournalConverter()}, - //{typeof (TimeEntryActivity), new TimeEntryActivityConverter()}, - //{typeof (IssuePriority), new IssuePriorityConverter()}, - //{typeof (WikiPage), new WikiPageConverter()}, - //{typeof (Detail), new DetailConverter()}, - //{typeof (ChangeSet), new ChangeSetConverter()}, - //{typeof (Membership), new MembershipConverter()}, - //{typeof (MembershipRole), new MembershipRoleConverter()}, - //{typeof (IdentifiableName), new IdentifiableNameConverter()}, - //{typeof (Permission), new PermissionConverter()}, - //{typeof (IssueChild), new IssueChildConverter()}, - //{typeof (ProjectIssueCategory), new ProjectIssueCategoryConverter()}, - //{typeof (Watcher), new WatcherConverter()}, - //{typeof (Upload), new UploadConverter()}, - //{typeof (ProjectEnabledModule), new ProjectEnabledModuleConverter()}, - //{typeof (CustomField), new CustomFieldConverter()}, - //{typeof (CustomFieldRole), new CustomFieldRoleConverter()}, - //{typeof (CustomFieldPossibleValue), new CustomFieldPossibleValueConverter()} - }; - - public static Dictionary JsonConverters { get { return jsonConverters; } } - - public static string JsonSerializer(T type) where T : new() - { - var sb = new StringBuilder(); - - using (var sw = new StringWriter(sb)) - { - using (JsonWriter writer = new JsonTextWriter(sw)) - { - writer.Formatting = Formatting.Indented; - var converter = jsonConverters[typeof(T)]; - var serializer = new JsonSerializer(); - converter.Serialize(writer, type, serializer); - - return sb.ToString(); - } - } - } - - /// - /// JSON Deserialization - /// - public static List JsonDeserializeToList(string jsonString, string root) where T : class, new() - { - int totalCount; - return JsonDeserializeToList(jsonString, root, out totalCount); - } - - /// - /// JSON Deserialization - /// - public static List JsonDeserializeToList(string jsonString, string root, out int totalCount) where T : class,new() - { - var result = JsonDeserializeToList(jsonString, root, typeof(T), out totalCount); - return result == null ? null : ((ArrayList)result).OfType().ToList(); - } - - public static T JsonDeserialize(string jsonString, string root) where T : new() - { - var type = typeof(T); - var result = JsonDeserialize(jsonString, type, root); - - if (result == null) return default(T); - - return (T)result; - } - - public static object JsonDeserialize(string jsonString, Type type, string root) - { - if (string.IsNullOrEmpty(jsonString)) return null; - - var serializer = new JsonSerializer(); - var converter = jsonConverters[type]; - var jObject = JObject.Parse(jsonString); - var rootName = root ?? type.Name.ToLowerInvariant(); - var rootObject = jObject[rootName]; - - if (rootObject == null) return null; - - jObject = JObject.Parse(rootObject.ToString()); - - return converter.Deserialize(jObject, serializer); - } - - private static void AddToList(JsonSerializer serializer, IList list, Type type, JToken obj) - { - //foreach (var item in obj.ToObject()) - //{ - // if (item is ArrayList) - // { - // AddToList(serializer, list, type, new JToken(item)); - // } - // else - // { - // var converter = jsonConverters[type]; - // var o = converter.Deserialize(obj, serializer); - - // list.Add(o); - // } - //} - } - - private static object JsonDeserializeToList(string jsonString, string root, Type type, out int totalCount) - { - totalCount = 0; - - if (string.IsNullOrEmpty(jsonString)) return null; - - var serializer = new JsonSerializer(); - var converter = jsonConverters[type]; - var jObject = JObject.Parse(jsonString); - - JToken obj, tc; - - if (jObject.TryGetValue(RedmineKeys.TOTAL_COUNT, out tc)) totalCount = tc.Value(); - if (!jObject.TryGetValue(root.ToLowerInvariant(), out obj)) return null; - - jObject = JObject.Parse(obj.ToString()); - - var result = converter.Deserialize(jObject, serializer); - var arrayList = new ArrayList(); - - if (type == typeof(Error)) - { - string info = null; - - foreach (var item in jObject.ToObject()) - { - var innerArrayList = item as ArrayList; - if (innerArrayList != null) - { - info = innerArrayList.Cast() - .Aggregate(info, (current, item2) => current + (item2 as string + " ")); - } - else - { - info += item as string + " "; - } - } - - var err = new Error { Info = info }; - arrayList.Add(err); - } - else - { - AddToList(serializer, arrayList, type, obj); - } - - return arrayList; - } - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/AttachmentConverter.cs b/redmine-net40-api/JSonConverters/AttachmentConverter.cs deleted file mode 100755 index 013d2063..00000000 --- a/redmine-net40-api/JSonConverters/AttachmentConverter.cs +++ /dev/null @@ -1,96 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - - -namespace Redmine.Net.Api.JSonConverters -{ - /// - /// - /// - /// - internal class AttachmentConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// An instance of property data stored as name/value pairs. - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var attachment = new Attachment(); - - attachment.Id = dictionary.GetValue(RedmineKeys.ID); - attachment.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); - attachment.Author = dictionary.GetValueAsIdentifiableName(RedmineKeys.AUTHOR); - attachment.ContentType = dictionary.GetValue(RedmineKeys.CONTENT_TYPE); - attachment.ContentUrl = dictionary.GetValue(RedmineKeys.CONTENT_URL); - attachment.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - attachment.FileName = dictionary.GetValue(RedmineKeys.FILENAME); - attachment.FileSize = dictionary.GetValue(RedmineKeys.FILESIZE); - - return attachment; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Attachment; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.FILENAME, entity.FileName); - result.Add(RedmineKeys.DESCRIPTION, entity.Description); - } - - var root = new Dictionary(); - root[RedmineKeys.ATTACHMENT] = result; - - return root; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(Attachment) }); } } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/AttachmentsConverter.cs b/redmine-net40-api/JSonConverters/AttachmentsConverter.cs deleted file mode 100755 index e75eb040..00000000 --- a/redmine-net40-api/JSonConverters/AttachmentsConverter.cs +++ /dev/null @@ -1,79 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class AttachmentsConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// An instance of property data stored as name/value pairs. - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object’s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Attachments; - var result = new Dictionary(); - - if (entity != null) - { - foreach (var entry in entity) - { - var attachment = new AttachmentConverter().Serialize(entry.Value, serializer); - result.Add(entry.Key.ToString(), attachment.First().Value); - } - } - - var root = new Dictionary(); - root[RedmineKeys.ATTACHMENTS] = result; - - return root; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(Attachments) }); } } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/ChangeSetConverter.cs b/redmine-net40-api/JSonConverters/ChangeSetConverter.cs deleted file mode 100755 index 4f4fcd0f..00000000 --- a/redmine-net40-api/JSonConverters/ChangeSetConverter.cs +++ /dev/null @@ -1,81 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ChangeSetConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// An instance of property data stored as name/value pairs. - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var changeSet = new ChangeSet - { - Revision = dictionary.GetValue(RedmineKeys.REVISION), - Comments = dictionary.GetValue(RedmineKeys.COMMENTS), - User = dictionary.GetValueAsIdentifiableName(RedmineKeys.USER), - CommittedOn = dictionary.GetValue(RedmineKeys.COMMITTED_ON) - }; - - - return changeSet; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(ChangeSet)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/CustomFieldConverter.cs b/redmine-net40-api/JSonConverters/CustomFieldConverter.cs deleted file mode 100644 index 510c230b..00000000 --- a/redmine-net40-api/JSonConverters/CustomFieldConverter.cs +++ /dev/null @@ -1,92 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class CustomFieldConverter : IdentifiableNameConverter - { - #region Overrides of JavaScriptConverter - - /// - /// Deserializes the specified dictionary. - /// - /// The dictionary. - /// The type. - /// The serializer. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var customField = new CustomField(); - - customField.Id = dictionary.GetValue(RedmineKeys.ID); - customField.Name = dictionary.GetValue(RedmineKeys.NAME); - customField.CustomizedType = dictionary.GetValue(RedmineKeys.CUSTOMIZED_TYPE); - customField.FieldFormat = dictionary.GetValue(RedmineKeys.FIELD_FORMAT); - customField.Regexp = dictionary.GetValue(RedmineKeys.REGEXP); - customField.MinLength = dictionary.GetValue(RedmineKeys.MIN_LENGTH); - customField.MaxLength = dictionary.GetValue(RedmineKeys.MAX_LENGTH); - customField.IsRequired = dictionary.GetValue(RedmineKeys.IS_REQUIRED); - customField.IsFilter = dictionary.GetValue(RedmineKeys.IS_FILTER); - customField.Searchable = dictionary.GetValue(RedmineKeys.SEARCHABLE); - customField.Multiple = dictionary.GetValue(RedmineKeys.MULTIPLE); - customField.DefaultValue = dictionary.GetValue(RedmineKeys.DEFAULT_VALUE); - customField.Visible = dictionary.GetValue(RedmineKeys.VISIBLE); - customField.PossibleValues = - dictionary.GetValueAsCollection(RedmineKeys.POSSIBLE_VALUES); - customField.Trackers = dictionary.GetValueAsCollection(RedmineKeys.TRACKERS); - customField.Roles = dictionary.GetValueAsCollection(RedmineKeys.ROLES); - - - return customField; - } - - return null; - } - - /// - /// Serializes the specified object. - /// - /// The object. - /// The serializer. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// Gets the supported types. - /// - /// - /// The supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(CustomField)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/CustomFieldPossibleValueConverter.cs b/redmine-net40-api/JSonConverters/CustomFieldPossibleValueConverter.cs deleted file mode 100644 index ff1a6aca..00000000 --- a/redmine-net40-api/JSonConverters/CustomFieldPossibleValueConverter.cs +++ /dev/null @@ -1,75 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class CustomFieldPossibleValueConverter : IdentifiableNameConverter - { - #region Overrides of JavaScriptConverter - - /// - /// Deserializes the specified dictionary. - /// - /// The dictionary. - /// The type. - /// The serializer. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var entity = new CustomFieldPossibleValue(); - - entity.Value = dictionary.GetValue(RedmineKeys.VALUE); - - return entity; - } - - return null; - } - - /// - /// Serializes the specified object. - /// - /// The object. - /// The serializer. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// Gets the supported types. - /// - /// - /// The supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(CustomFieldPossibleValue)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/CustomFieldRoleConverter.cs b/redmine-net40-api/JSonConverters/CustomFieldRoleConverter.cs deleted file mode 100644 index 6d2504ca..00000000 --- a/redmine-net40-api/JSonConverters/CustomFieldRoleConverter.cs +++ /dev/null @@ -1,76 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class CustomFieldRoleConverter : IdentifiableNameConverter - { - #region Overrides of JavaScriptConverter - - /// - /// Deserializes the specified dictionary. - /// - /// The dictionary. - /// The type. - /// The serializer. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var entity = new CustomFieldRole(); - - entity.Id = dictionary.GetValue(RedmineKeys.ID); - entity.Name = dictionary.GetValue(RedmineKeys.NAME); - - return entity; - } - - return null; - } - - /// - /// Serializes the specified object. - /// - /// The object. - /// The serializer. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// Gets the supported types. - /// - /// - /// The supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(CustomFieldRole)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/DetailConverter.cs b/redmine-net40-api/JSonConverters/DetailConverter.cs deleted file mode 100755 index 6f6b7bb9..00000000 --- a/redmine-net40-api/JSonConverters/DetailConverter.cs +++ /dev/null @@ -1,82 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class DetailConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var detail = new Detail(); - - detail.NewValue = dictionary.GetValue(RedmineKeys.NEW_VALUE); - detail.OldValue = dictionary.GetValue(RedmineKeys.OLD_VALUE); - detail.Property = dictionary.GetValue(RedmineKeys.PROPERTY); - detail.Name = dictionary.GetValue(RedmineKeys.NAME); - - return detail; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(Detail)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/ErrorConverter.cs b/redmine-net40-api/JSonConverters/ErrorConverter.cs deleted file mode 100755 index 1bcc1f00..00000000 --- a/redmine-net40-api/JSonConverters/ErrorConverter.cs +++ /dev/null @@ -1,76 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ErrorConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var error = new Error {Info = dictionary.GetValue(RedmineKeys.ERROR)}; - return error; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(Error)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/FileConverter.cs b/redmine-net40-api/JSonConverters/FileConverter.cs deleted file mode 100644 index 8ce81be5..00000000 --- a/redmine-net40-api/JSonConverters/FileConverter.cs +++ /dev/null @@ -1,111 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class FileConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var file = new File { }; - - file.Author = dictionary.GetValueAsIdentifiableName(RedmineKeys.AUTHOR); - file.ContentType = dictionary.GetValue(RedmineKeys.CONTENT_TYPE); - file.ContentUrl = dictionary.GetValue(RedmineKeys.CONTENT_URL); - file.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - file.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); - file.Digest = dictionary.GetValue(RedmineKeys.DIGEST); - file.Downloads = dictionary.GetValue(RedmineKeys.DOWNLOADS); - file.Filename = dictionary.GetValue(RedmineKeys.FILENAME); - file.Filesize = dictionary.GetValue(RedmineKeys.FILESIZE); - file.Id = dictionary.GetValue(RedmineKeys.ID); - file.Token = dictionary.GetValue(RedmineKeys.TOKEN); - var versionId = dictionary.GetValue(RedmineKeys.VERSION_ID); - if (versionId.HasValue) - { - file.Version = new IdentifiableName { Id = versionId.Value }; - } - else - { - file.Version = dictionary.GetValueAsIdentifiableName(RedmineKeys.VERSION); - } - return file; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object’s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as File; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.TOKEN, entity.Token); - result.WriteIdIfNotNull(entity.Version, RedmineKeys.VERSION_ID); - result.Add(RedmineKeys.FILENAME, entity.Filename); - result.Add(RedmineKeys.DESCRIPTION, entity.Description); - - var root = new Dictionary(); - root[RedmineKeys.FILE] = result; - return root; - } - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] { typeof(File) }); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/GroupConverter.cs b/redmine-net40-api/JSonConverters/GroupConverter.cs deleted file mode 100755 index cb924b86..00000000 --- a/redmine-net40-api/JSonConverters/GroupConverter.cs +++ /dev/null @@ -1,97 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class GroupConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var group = new Group(); - - group.Id = dictionary.GetValue(RedmineKeys.ID); - group.Name = dictionary.GetValue(RedmineKeys.NAME); - group.Users = dictionary.GetValueAsCollection(RedmineKeys.USERS); - group.CustomFields = dictionary.GetValueAsCollection(RedmineKeys.CUSTOM_FIELDS); - group.Memberships = dictionary.GetValueAsCollection(RedmineKeys.MEMBERSHIPS); - - return group; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Group; - - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.NAME, entity.Name); - result.WriteIdsArray(RedmineKeys.USER_IDS, entity.Users); - - var root = new Dictionary(); - root[RedmineKeys.GROUP] = result; - return root; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(Group)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/GroupUserConverter.cs b/redmine-net40-api/JSonConverters/GroupUserConverter.cs deleted file mode 100644 index c12875e3..00000000 --- a/redmine-net40-api/JSonConverters/GroupUserConverter.cs +++ /dev/null @@ -1,77 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - - -namespace Redmine.Net.Api.JSonConverters -{ - internal class GroupUserConverter : IdentifiableNameConverter - { - #region Overrides of JavaScriptConverter - - /// - /// Deserializes the specified dictionary. - /// - /// The dictionary. - /// The type. - /// The serializer. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var userGroup = new GroupUser(); - - userGroup.Id = dictionary.GetValue(RedmineKeys.ID); - userGroup.Name = dictionary.GetValue(RedmineKeys.NAME); - - return userGroup; - } - - return null; - } - - /// - /// Serializes the specified object. - /// - /// The object. - /// The serializer. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// Gets the supported types. - /// - /// - /// The supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(GroupUser)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/IdentifiableNameConverter.cs b/redmine-net40-api/JSonConverters/IdentifiableNameConverter.cs deleted file mode 100755 index 48d44b4d..00000000 --- a/redmine-net40-api/JSonConverters/IdentifiableNameConverter.cs +++ /dev/null @@ -1,91 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IdentifiableNameConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var entity = new IdentifiableName(); - - entity.Id = dictionary.GetValue(RedmineKeys.ID); - entity.Name = dictionary.GetValue(RedmineKeys.NAME); - - return entity; - } - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as IdentifiableName; - var result = new Dictionary(); - - if (entity != null) - { - result.WriteIdIfNotNull(entity, RedmineKeys.ID); - - if (!string.IsNullOrEmpty(entity.Name)) - result.Add(RedmineKeys.NAME, entity.Name); - return result; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(IdentifiableName)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/IssueCategoryConverter.cs b/redmine-net40-api/JSonConverters/IssueCategoryConverter.cs deleted file mode 100755 index 2c173d36..00000000 --- a/redmine-net40-api/JSonConverters/IssueCategoryConverter.cs +++ /dev/null @@ -1,97 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssueCategoryConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var issueCategory = new IssueCategory(); - - issueCategory.Id = dictionary.GetValue(RedmineKeys.ID); - issueCategory.Project = dictionary.GetValueAsIdentifiableName(RedmineKeys.PROJECT); - issueCategory.AsignTo = dictionary.GetValueAsIdentifiableName(RedmineKeys.ASSIGNED_TO); - issueCategory.Name = dictionary.GetValue(RedmineKeys.NAME); - - return issueCategory; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as IssueCategory; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.NAME, entity.Name); - result.WriteIdIfNotNull(entity.Project, RedmineKeys.PROJECT_ID); - result.WriteIdIfNotNull(entity.AsignTo, RedmineKeys.ASSIGNED_TO_ID); - - var root = new Dictionary(); - - root[RedmineKeys.ISSUE_CATEGORY] = result; - return root; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(IssueCategory)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/IssueChildConverter.cs b/redmine-net40-api/JSonConverters/IssueChildConverter.cs deleted file mode 100755 index 718119fe..00000000 --- a/redmine-net40-api/JSonConverters/IssueChildConverter.cs +++ /dev/null @@ -1,75 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssueChildConverter : JavaScriptConverter - { - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(IssueChild)}); } - } - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// An instance of property data stored as name/value pairs. - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var issueChild = new IssueChild - { - Id = dictionary.GetValue(RedmineKeys.ID), - Tracker = dictionary.GetValueAsIdentifiableName(RedmineKeys.TRACKER), - Subject = dictionary.GetValue(RedmineKeys.SUBJECT) - }; - - return issueChild; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/IssueConverter.cs b/redmine-net40-api/JSonConverters/IssueConverter.cs deleted file mode 100644 index a032ae21..00000000 --- a/redmine-net40-api/JSonConverters/IssueConverter.cs +++ /dev/null @@ -1,155 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssueConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var issue = new Issue(); - - issue.Id = dictionary.GetValue(RedmineKeys.ID); - issue.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); - issue.Project = dictionary.GetValueAsIdentifiableName(RedmineKeys.PROJECT); - issue.Tracker = dictionary.GetValueAsIdentifiableName(RedmineKeys.TRACKER); - issue.Status = dictionary.GetValueAsIdentifiableName(RedmineKeys.STATUS); - issue.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - issue.UpdatedOn = dictionary.GetValue(RedmineKeys.UPDATED_ON); - issue.ClosedOn = dictionary.GetValue(RedmineKeys.CLOSED_ON); - issue.Priority = dictionary.GetValueAsIdentifiableName(RedmineKeys.PRIORITY); - issue.Author = dictionary.GetValueAsIdentifiableName(RedmineKeys.AUTHOR); - issue.AssignedTo = dictionary.GetValueAsIdentifiableName(RedmineKeys.ASSIGNED_TO); - issue.Category = dictionary.GetValueAsIdentifiableName(RedmineKeys.CATEGORY); - issue.FixedVersion = dictionary.GetValueAsIdentifiableName(RedmineKeys.FIXED_VERSION); - issue.Subject = dictionary.GetValue(RedmineKeys.SUBJECT); - issue.Notes = dictionary.GetValue(RedmineKeys.NOTES); - issue.IsPrivate = dictionary.GetValue(RedmineKeys.IS_PRIVATE); - issue.StartDate = dictionary.GetValue(RedmineKeys.START_DATE); - issue.DueDate = dictionary.GetValue(RedmineKeys.DUE_DATE); - issue.SpentHours = dictionary.GetValue(RedmineKeys.SPENT_HOURS); - issue.TotalSpentHours = dictionary.GetValue(RedmineKeys.TOTAL_SPENT_HOURS); - issue.DoneRatio = dictionary.GetValue(RedmineKeys.DONE_RATIO); - issue.EstimatedHours = dictionary.GetValue(RedmineKeys.ESTIMATED_HOURS); - issue.TotalEstimatedHours = dictionary.GetValue(RedmineKeys.TOTAL_ESTIMATED_HOURS); - issue.ParentIssue = dictionary.GetValueAsIdentifiableName(RedmineKeys.PARENT); - - issue.CustomFields = dictionary.GetValueAsCollection(RedmineKeys.CUSTOM_FIELDS); - issue.Attachments = dictionary.GetValueAsCollection(RedmineKeys.ATTACHMENTS); - issue.Relations = dictionary.GetValueAsCollection(RedmineKeys.RELATIONS); - issue.Journals = dictionary.GetValueAsCollection(RedmineKeys.JOURNALS); - issue.Changesets = dictionary.GetValueAsCollection(RedmineKeys.CHANGESETS); - issue.Watchers = dictionary.GetValueAsCollection(RedmineKeys.WATCHERS); - issue.Children = dictionary.GetValueAsCollection(RedmineKeys.CHILDREN); - return issue; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Issue; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.SUBJECT, entity.Subject); - result.Add(RedmineKeys.DESCRIPTION, entity.Description); - result.Add(RedmineKeys.NOTES, entity.Notes); - if (entity.Id != 0) - { - result.Add(RedmineKeys.PRIVATE_NOTES, entity.PrivateNotes.ToString().ToLowerInvariant()); - } - result.Add(RedmineKeys.IS_PRIVATE, entity.IsPrivate.ToString().ToLowerInvariant()); - result.WriteIdIfNotNull(entity.Project, RedmineKeys.PROJECT_ID); - result.WriteIdIfNotNull(entity.Priority, RedmineKeys.PRIORITY_ID); - result.WriteIdIfNotNull(entity.Status, RedmineKeys.STATUS_ID); - result.WriteIdIfNotNull(entity.Category, RedmineKeys.CATEGORY_ID); - result.WriteIdIfNotNull(entity.Tracker, RedmineKeys.TRACKER_ID); - result.WriteIdIfNotNull(entity.AssignedTo, RedmineKeys.ASSIGNED_TO_ID); - result.WriteIdIfNotNull(entity.FixedVersion, RedmineKeys.FIXED_VERSION_ID); - result.WriteValueOrEmpty(entity.EstimatedHours, RedmineKeys.ESTIMATED_HOURS); - - result.WriteIdOrEmpty(entity.ParentIssue, RedmineKeys.PARENT_ISSUE_ID); - result.WriteDateOrEmpty(entity.StartDate, RedmineKeys.START_DATE); - result.WriteDateOrEmpty(entity.DueDate, RedmineKeys.DUE_DATE); - result.WriteDateOrEmpty(entity.DueDate, RedmineKeys.UPDATED_ON); - - if (entity.DoneRatio != null) - result.Add(RedmineKeys.DONE_RATIO, entity.DoneRatio.Value.ToString(CultureInfo.InvariantCulture)); - - if (entity.SpentHours != null) - result.Add(RedmineKeys.SPENT_HOURS, entity.SpentHours.Value.ToString(CultureInfo.InvariantCulture)); - - result.WriteArray(RedmineKeys.UPLOADS, entity.Uploads, new UploadConverter(), serializer); - result.WriteArray(RedmineKeys.CUSTOM_FIELDS, entity.CustomFields, new IssueCustomFieldConverter(), - serializer); - - result.WriteIdsArray(RedmineKeys.WATCHER_USER_IDS, entity.Watchers); - - var root = new Dictionary(); - root[RedmineKeys.ISSUE] = result; - return root; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(Issue)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/IssueCustomFieldConverter.cs b/redmine-net40-api/JSonConverters/IssueCustomFieldConverter.cs deleted file mode 100755 index 8ba8c10c..00000000 --- a/redmine-net40-api/JSonConverters/IssueCustomFieldConverter.cs +++ /dev/null @@ -1,119 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssueCustomFieldConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var customField = new IssueCustomField(); - - customField.Id = dictionary.GetValue(RedmineKeys.ID); - customField.Name = dictionary.GetValue(RedmineKeys.NAME); - customField.Multiple = dictionary.GetValue(RedmineKeys.MULTIPLE); - - var val = dictionary.GetValue(RedmineKeys.VALUE); - - if (val != null) - { - if (customField.Values == null) customField.Values = new List(); - var list = val as ArrayList; - if (list != null) - { - foreach (var value in list) - { - customField.Values.Add(new CustomFieldValue {Info = Convert.ToString(value)}); - } - } - else - { - customField.Values.Add(new CustomFieldValue {Info = Convert.ToString(val)}); - } - } - return customField; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as IssueCustomField; - - var result = new Dictionary(); - - if (entity == null) return result; - if (entity.Values == null) return null; - var itemsCount = entity.Values.Count; - - result.Add(RedmineKeys.ID, entity.Id); - if (itemsCount > 1) - { - result.Add(RedmineKeys.VALUE, entity.Values.Select(x => x.Info).ToArray()); - } - else - { - result.Add(RedmineKeys.VALUE, itemsCount > 0 ? entity.Values[0].Info : null); - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(IssueCustomField)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/IssuePriorityConverter.cs b/redmine-net40-api/JSonConverters/IssuePriorityConverter.cs deleted file mode 100755 index b3f6ac6f..00000000 --- a/redmine-net40-api/JSonConverters/IssuePriorityConverter.cs +++ /dev/null @@ -1,77 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssuePriorityConverter : JavaScriptConverter - { - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(IssuePriority)}); } - } - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var issuePriority = new IssuePriority(); - - issuePriority.Id = dictionary.GetValue(RedmineKeys.ID); - issuePriority.Name = dictionary.GetValue(RedmineKeys.NAME); - issuePriority.IsDefault = dictionary.GetValue(RedmineKeys.IS_DEFAULT); - - return issuePriority; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/IssueRelationConverter.cs b/redmine-net40-api/JSonConverters/IssueRelationConverter.cs deleted file mode 100755 index 14ce53b4..00000000 --- a/redmine-net40-api/JSonConverters/IssueRelationConverter.cs +++ /dev/null @@ -1,99 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssueRelationConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var issueRelation = new IssueRelation(); - - issueRelation.Id = dictionary.GetValue(RedmineKeys.ID); - issueRelation.IssueId = dictionary.GetValue(RedmineKeys.ISSUE_ID); - issueRelation.IssueToId = dictionary.GetValue(RedmineKeys.ISSUE_TO_ID); - issueRelation.Type = dictionary.GetValue(RedmineKeys.RELATION_TYPE); - issueRelation.Delay = dictionary.GetValue(RedmineKeys.DELAY); - - return issueRelation; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as IssueRelation; - - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.ISSUE_TO_ID, entity.IssueToId); - result.Add(RedmineKeys.RELATION_TYPE, entity.Type.ToString()); - if (entity.Type == IssueRelationType.precedes || entity.Type == IssueRelationType.follows) - result.WriteValueOrEmpty(entity.Delay, RedmineKeys.DELAY); - - var root = new Dictionary(); - root[RedmineKeys.RELATION] = result; - return root; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(IssueRelation)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/IssueStatusConverter.cs b/redmine-net40-api/JSonConverters/IssueStatusConverter.cs deleted file mode 100755 index 8f806cc9..00000000 --- a/redmine-net40-api/JSonConverters/IssueStatusConverter.cs +++ /dev/null @@ -1,81 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssueStatusConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var issueStatus = new IssueStatus(); - - issueStatus.Id = dictionary.GetValue(RedmineKeys.ID); - issueStatus.Name = dictionary.GetValue(RedmineKeys.NAME); - issueStatus.IsClosed = dictionary.GetValue(RedmineKeys.IS_CLOSED); - issueStatus.IsDefault = dictionary.GetValue(RedmineKeys.IS_DEFAULT); - return issueStatus; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(IssueStatus)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/JournalConverter.cs b/redmine-net40-api/JSonConverters/JournalConverter.cs deleted file mode 100755 index b37f4621..00000000 --- a/redmine-net40-api/JSonConverters/JournalConverter.cs +++ /dev/null @@ -1,83 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class JournalConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var journal = new Journal(); - - journal.Id = dictionary.GetValue(RedmineKeys.ID); - journal.Notes = dictionary.GetValue(RedmineKeys.NOTES); - journal.User = dictionary.GetValueAsIdentifiableName(RedmineKeys.USER); - journal.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - journal.Details = dictionary.GetValueAsCollection(RedmineKeys.DETAILS); - - return journal; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(Journal)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/MembershipConverter.cs b/redmine-net40-api/JSonConverters/MembershipConverter.cs deleted file mode 100755 index b213233a..00000000 --- a/redmine-net40-api/JSonConverters/MembershipConverter.cs +++ /dev/null @@ -1,81 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class MembershipConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var membership = new Membership(); - - membership.Id = dictionary.GetValue(RedmineKeys.ID); - membership.Project = dictionary.GetValueAsIdentifiableName(RedmineKeys.PROJECT); - membership.Roles = dictionary.GetValueAsCollection(RedmineKeys.ROLES); - - return membership; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(Membership)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/MembershipRoleConverter.cs b/redmine-net40-api/JSonConverters/MembershipRoleConverter.cs deleted file mode 100755 index 3ab0f170..00000000 --- a/redmine-net40-api/JSonConverters/MembershipRoleConverter.cs +++ /dev/null @@ -1,81 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class MembershipRoleConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var membershipRole = new MembershipRole(); - - membershipRole.Id = dictionary.GetValue(RedmineKeys.ID); - membershipRole.Inherited = dictionary.GetValue(RedmineKeys.INHERITED); - membershipRole.Name = dictionary.GetValue(RedmineKeys.NAME); - - return membershipRole; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(MembershipRole)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/NewsConverter.cs b/redmine-net40-api/JSonConverters/NewsConverter.cs deleted file mode 100755 index c5922d16..00000000 --- a/redmine-net40-api/JSonConverters/NewsConverter.cs +++ /dev/null @@ -1,84 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class NewsConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var news = new News(); - - news.Id = dictionary.GetValue(RedmineKeys.ID); - news.Author = dictionary.GetValueAsIdentifiableName(RedmineKeys.AUTHOR); - news.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - news.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); - news.Project = dictionary.GetValueAsIdentifiableName(RedmineKeys.PROJECT); - news.Summary = dictionary.GetValue(RedmineKeys.SUMMARY); - news.Title = dictionary.GetValue(RedmineKeys.TITLE); - - return news; - } - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(News)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/PermissionConverter.cs b/redmine-net40-api/JSonConverters/PermissionConverter.cs deleted file mode 100755 index b02f432d..00000000 --- a/redmine-net40-api/JSonConverters/PermissionConverter.cs +++ /dev/null @@ -1,72 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class PermissionConverter : JavaScriptConverter - { - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(Permission)}); } - } - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var permission = new Permission {Info = dictionary.GetValue(RedmineKeys.PERMISSION)}; - return permission; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/ProjectConverter.cs b/redmine-net40-api/JSonConverters/ProjectConverter.cs deleted file mode 100644 index 4d98dc77..00000000 --- a/redmine-net40-api/JSonConverters/ProjectConverter.cs +++ /dev/null @@ -1,116 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ProjectConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var project = new Project(); - - project.Id = dictionary.GetValue(RedmineKeys.ID); - project.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); - project.HomePage = dictionary.GetValue(RedmineKeys.HOMEPAGE); - project.Name = dictionary.GetValue(RedmineKeys.NAME); - project.Identifier = dictionary.GetValue(RedmineKeys.IDENTIFIER); - project.Status = dictionary.GetValue(RedmineKeys.STATUS); - project.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - project.UpdatedOn = dictionary.GetValue(RedmineKeys.UPDATED_ON); - project.Trackers = dictionary.GetValueAsCollection(RedmineKeys.TRACKERS); - project.CustomFields = dictionary.GetValueAsCollection(RedmineKeys.CUSTOM_FIELDS); - project.IsPublic = dictionary.GetValue(RedmineKeys.IS_PUBLIC); - project.Parent = dictionary.GetValueAsIdentifiableName(RedmineKeys.PARENT); - project.IssueCategories = dictionary.GetValueAsCollection(RedmineKeys.ISSUE_CATEGORIES); - project.EnabledModules = dictionary.GetValueAsCollection(RedmineKeys.ENABLED_MODULES); - project.TimeEntryActivities = dictionary.GetValueAsCollection(RedmineKeys.TIME_ENTRY_ACTIVITIES); - return project; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object’s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Project; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.NAME, entity.Name); - result.Add(RedmineKeys.IDENTIFIER, entity.Identifier); - result.Add(RedmineKeys.DESCRIPTION, entity.Description); - result.Add(RedmineKeys.HOMEPAGE, entity.HomePage); - result.Add(RedmineKeys.INHERIT_MEMBERS, entity.InheritMembers.ToString().ToLowerInvariant()); - result.Add(RedmineKeys.IS_PUBLIC, entity.IsPublic.ToString().ToLowerInvariant()); - result.WriteIdOrEmpty(entity.Parent, RedmineKeys.PARENT_ID, string.Empty); - result.WriteIdsArray(RedmineKeys.TRACKER_IDS, entity.Trackers); - result.WriteNamesArray(RedmineKeys.ENABLED_MODULE_NAMES, entity.EnabledModules); - if (entity.Id > 0) - { - result.WriteArray(RedmineKeys.CUSTOM_FIELDS, entity.CustomFields, new IssueCustomFieldConverter(), - serializer); - } - var root = new Dictionary(); - root[RedmineKeys.PROJECT] = result; - return root; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] { typeof(Project) }); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/ProjectEnabledModuleConverter.cs b/redmine-net40-api/JSonConverters/ProjectEnabledModuleConverter.cs deleted file mode 100755 index 11227520..00000000 --- a/redmine-net40-api/JSonConverters/ProjectEnabledModuleConverter.cs +++ /dev/null @@ -1,77 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ProjectEnabledModuleConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var projectEnableModule = new ProjectEnabledModule(); - projectEnableModule.Id = dictionary.GetValue(RedmineKeys.ID); - projectEnableModule.Name = dictionary.GetValue(RedmineKeys.NAME); - return projectEnableModule; - } - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(ProjectEnabledModule)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/ProjectIssueCategoryConverter.cs b/redmine-net40-api/JSonConverters/ProjectIssueCategoryConverter.cs deleted file mode 100755 index d7956dcb..00000000 --- a/redmine-net40-api/JSonConverters/ProjectIssueCategoryConverter.cs +++ /dev/null @@ -1,89 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ProjectIssueCategoryConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var projectTracker = new ProjectIssueCategory(); - projectTracker.Id = dictionary.GetValue(RedmineKeys.ID); - projectTracker.Name = dictionary.GetValue(RedmineKeys.NAME); - return projectTracker; - } - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as ProjectIssueCategory; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.ID, entity.Id); - result.Add(RedmineKeys.NAME, entity.Name); - - var root = new Dictionary(); - root[RedmineKeys.ISSUE_CATEGORY] = result; - return root; - } - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(ProjectIssueCategory)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/ProjectMembershipConverter.cs b/redmine-net40-api/JSonConverters/ProjectMembershipConverter.cs deleted file mode 100755 index 7b513870..00000000 --- a/redmine-net40-api/JSonConverters/ProjectMembershipConverter.cs +++ /dev/null @@ -1,95 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ProjectMembershipConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var projectMembership = new ProjectMembership(); - - projectMembership.Id = dictionary.GetValue(RedmineKeys.ID); - projectMembership.Group = dictionary.GetValueAsIdentifiableName(RedmineKeys.GROUP); - projectMembership.Project = dictionary.GetValueAsIdentifiableName(RedmineKeys.PROJECT); - projectMembership.Roles = dictionary.GetValueAsCollection(RedmineKeys.ROLES); - projectMembership.User = dictionary.GetValueAsIdentifiableName(RedmineKeys.USER); - - return projectMembership; - } - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as ProjectMembership; - var result = new Dictionary(); - - if (entity != null) - { - result.WriteIdIfNotNull(entity.User, RedmineKeys.USER_ID); - result.WriteIdsArray(RedmineKeys.ROLE_IDS, entity.Roles); - - var root = new Dictionary(); - root[RedmineKeys.MEMBERSHIP] = result; - return root; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(ProjectMembership)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/ProjectTrackerConverter.cs b/redmine-net40-api/JSonConverters/ProjectTrackerConverter.cs deleted file mode 100755 index 5f205d44..00000000 --- a/redmine-net40-api/JSonConverters/ProjectTrackerConverter.cs +++ /dev/null @@ -1,77 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ProjectTrackerConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var projectTracker = new ProjectTracker(); - projectTracker.Id = dictionary.GetValue(RedmineKeys.ID); - projectTracker.Name = dictionary.GetValue(RedmineKeys.NAME); - return projectTracker; - } - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(ProjectTracker)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/QueryConverter.cs b/redmine-net40-api/JSonConverters/QueryConverter.cs deleted file mode 100755 index 4cabeb5e..00000000 --- a/redmine-net40-api/JSonConverters/QueryConverter.cs +++ /dev/null @@ -1,82 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class QueryConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var query = new Query(); - - query.Id = dictionary.GetValue(RedmineKeys.ID); - query.IsPublic = dictionary.GetValue(RedmineKeys.IS_PUBLIC); - query.ProjectId = dictionary.GetValue(RedmineKeys.PROJECT_ID); - query.Name = dictionary.GetValue(RedmineKeys.NAME); - - return query; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(Query)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/RoleConverter.cs b/redmine-net40-api/JSonConverters/RoleConverter.cs deleted file mode 100755 index d57464bb..00000000 --- a/redmine-net40-api/JSonConverters/RoleConverter.cs +++ /dev/null @@ -1,91 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class RoleConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var role = new Role(); - - role.Id = dictionary.GetValue(RedmineKeys.ID); - role.Name = dictionary.GetValue(RedmineKeys.NAME); - - var permissions = dictionary.GetValue(RedmineKeys.PERMISSIONS); - if (permissions != null) - { - role.Permissions = new List(); - foreach (var permission in permissions) - { - var perms = new Permission {Info = permission.ToString()}; - role.Permissions.Add(perms); - } - } - - return role; - } - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(Role)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/TimeEntryActivityConverter.cs b/redmine-net40-api/JSonConverters/TimeEntryActivityConverter.cs deleted file mode 100755 index 87b6e5fc..00000000 --- a/redmine-net40-api/JSonConverters/TimeEntryActivityConverter.cs +++ /dev/null @@ -1,77 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class TimeEntryActivityConverter : JavaScriptConverter - { - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(TimeEntryActivity)}); } - } - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var timeEntryActivity = new TimeEntryActivity - { - Id = dictionary.GetValue(RedmineKeys.ID), - Name = dictionary.GetValue(RedmineKeys.NAME), - IsDefault = dictionary.GetValue(RedmineKeys.IS_DEFAULT) - }; - return timeEntryActivity; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/TimeEntryConverter.cs b/redmine-net40-api/JSonConverters/TimeEntryConverter.cs deleted file mode 100755 index 435e86ae..00000000 --- a/redmine-net40-api/JSonConverters/TimeEntryConverter.cs +++ /dev/null @@ -1,120 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class TimeEntryConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var timeEntry = new TimeEntry(); - - timeEntry.Id = dictionary.GetValue(RedmineKeys.ID); - timeEntry.Activity = - dictionary.GetValueAsIdentifiableName(dictionary.ContainsKey(RedmineKeys.ACTIVITY) - ? RedmineKeys.ACTIVITY - : RedmineKeys.ACTIVITY_ID); - timeEntry.Comments = dictionary.GetValue(RedmineKeys.COMMENTS); - timeEntry.Hours = dictionary.GetValue(RedmineKeys.HOURS); - timeEntry.Issue = - dictionary.GetValueAsIdentifiableName(dictionary.ContainsKey(RedmineKeys.ISSUE) - ? RedmineKeys.ISSUE - : RedmineKeys.ISSUE_ID); - timeEntry.Project = - dictionary.GetValueAsIdentifiableName(dictionary.ContainsKey(RedmineKeys.PROJECT) - ? RedmineKeys.PROJECT - : RedmineKeys.PROJECT_ID); - timeEntry.SpentOn = dictionary.GetValue(RedmineKeys.SPENT_ON); - timeEntry.User = dictionary.GetValueAsIdentifiableName(RedmineKeys.USER); - timeEntry.CustomFields = dictionary.GetValueAsCollection(RedmineKeys.CUSTOM_FIELDS); - timeEntry.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - timeEntry.UpdatedOn = dictionary.GetValue(RedmineKeys.UPDATED_ON); - - return timeEntry; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as TimeEntry; - var result = new Dictionary(); - - if (entity != null) - { - result.WriteIdIfNotNull(entity.Issue, RedmineKeys.ISSUE_ID); - result.WriteIdIfNotNull(entity.Project, RedmineKeys.PROJECT_ID); - result.WriteIdIfNotNull(entity.Activity, RedmineKeys.ACTIVITY_ID); - - if (!entity.SpentOn.HasValue) entity.SpentOn = DateTime.Now; - - result.WriteDateOrEmpty(entity.SpentOn, RedmineKeys.SPENT_ON); - result.Add(RedmineKeys.HOURS, entity.Hours); - result.Add(RedmineKeys.COMMENTS, entity.Comments); - result.WriteArray(RedmineKeys.CUSTOM_FIELDS, entity.CustomFields, new IssueCustomFieldConverter(), - serializer); - - var root = new Dictionary(); - root[RedmineKeys.TIME_ENTRY] = result; - return root; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(TimeEntry)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/TrackerConverter.cs b/redmine-net40-api/JSonConverters/TrackerConverter.cs deleted file mode 100755 index a972f662..00000000 --- a/redmine-net40-api/JSonConverters/TrackerConverter.cs +++ /dev/null @@ -1,80 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class TrackerConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var tracker = new Tracker - { - Id = dictionary.GetValue(RedmineKeys.ID), - Name = dictionary.GetValue(RedmineKeys.NAME) - }; - return tracker; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(Tracker)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/TrackerCustomFieldConverter.cs b/redmine-net40-api/JSonConverters/TrackerCustomFieldConverter.cs deleted file mode 100644 index 74b92a06..00000000 --- a/redmine-net40-api/JSonConverters/TrackerCustomFieldConverter.cs +++ /dev/null @@ -1,67 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class TrackerCustomFieldConverter : IdentifiableNameConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var entity = new TrackerCustomField(); - - entity.Id = dictionary.GetValue(RedmineKeys.ID); - entity.Name = dictionary.GetValue(RedmineKeys.NAME); - - return entity; - } - - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(TrackerCustomField)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/UploadConverter.cs b/redmine-net40-api/JSonConverters/UploadConverter.cs deleted file mode 100755 index 0b5bb165..00000000 --- a/redmine-net40-api/JSonConverters/UploadConverter.cs +++ /dev/null @@ -1,92 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class UploadConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var upload = new Upload(); - - upload.ContentType = dictionary.GetValue(RedmineKeys.CONTENT_TYPE); - upload.FileName = dictionary.GetValue(RedmineKeys.FILENAME); - upload.Token = dictionary.GetValue(RedmineKeys.TOKEN); - upload.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); - return upload; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Upload; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.CONTENT_TYPE, entity.ContentType); - result.Add(RedmineKeys.FILENAME, entity.FileName); - result.Add(RedmineKeys.TOKEN, entity.Token); - result.Add(RedmineKeys.DESCRIPTION, entity.Description); - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(Upload)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/UserConverter.cs b/redmine-net40-api/JSonConverters/UserConverter.cs deleted file mode 100755 index f1d6003c..00000000 --- a/redmine-net40-api/JSonConverters/UserConverter.cs +++ /dev/null @@ -1,112 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class UserConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var user = new User(); - user.Login = dictionary.GetValue(RedmineKeys.LOGIN); - user.Id = dictionary.GetValue(RedmineKeys.ID); - user.FirstName = dictionary.GetValue(RedmineKeys.FIRSTNAME); - user.LastName = dictionary.GetValue(RedmineKeys.LASTNAME); - user.Email = dictionary.GetValue(RedmineKeys.MAIL); - user.MailNotification = dictionary.GetValue(RedmineKeys.MAIL_NOTIFICATION); - user.AuthenticationModeId = dictionary.GetValue(RedmineKeys.AUTH_SOURCE_ID); - user.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - user.LastLoginOn = dictionary.GetValue(RedmineKeys.LAST_LOGIN_ON); - user.ApiKey = dictionary.GetValue(RedmineKeys.API_KEY); - user.Status = dictionary.GetValue(RedmineKeys.STATUS); - user.MustChangePassword = dictionary.GetValue(RedmineKeys.MUST_CHANGE_PASSWD); - user.CustomFields = dictionary.GetValueAsCollection(RedmineKeys.CUSTOM_FIELDS); - user.Memberships = dictionary.GetValueAsCollection(RedmineKeys.MEMBERSHIPS); - user.Groups = dictionary.GetValueAsCollection(RedmineKeys.GROUPS); - - return user; - } - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as User; - - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.LOGIN, entity.Login); - result.Add(RedmineKeys.FIRSTNAME, entity.FirstName); - result.Add(RedmineKeys.LASTNAME, entity.LastName); - result.Add(RedmineKeys.MAIL, entity.Email); - result.Add(RedmineKeys.MAIL_NOTIFICATION, entity.MailNotification); - result.Add(RedmineKeys.PASSWORD, entity.Password); - result.Add(RedmineKeys.MUST_CHANGE_PASSWD, entity.MustChangePassword.ToString().ToLowerInvariant()); - result.WriteValueOrEmpty(entity.AuthenticationModeId, RedmineKeys.AUTH_SOURCE_ID); - result.WriteArray(RedmineKeys.CUSTOM_FIELDS, entity.CustomFields, new IssueCustomFieldConverter(), - serializer); - - var root = new Dictionary(); - root[RedmineKeys.USER] = result; - return root; - } - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(User)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/UserGroupConverter.cs b/redmine-net40-api/JSonConverters/UserGroupConverter.cs deleted file mode 100755 index 43636af8..00000000 --- a/redmine-net40-api/JSonConverters/UserGroupConverter.cs +++ /dev/null @@ -1,67 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class UserGroupConverter : IdentifiableNameConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(UserGroup)}); } - } - - #endregion - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var userGroup = new UserGroup(); - - userGroup.Id = dictionary.GetValue(RedmineKeys.ID); - userGroup.Name = dictionary.GetValue(RedmineKeys.NAME); - - return userGroup; - } - - return null; - } - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/VersionConverter.cs b/redmine-net40-api/JSonConverters/VersionConverter.cs deleted file mode 100755 index 06b21466..00000000 --- a/redmine-net40-api/JSonConverters/VersionConverter.cs +++ /dev/null @@ -1,106 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; -using Version = Redmine.Net.Api.Types.Version; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class VersionConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var version = new Version(); - - version.Id = dictionary.GetValue(RedmineKeys.ID); - version.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); - version.Name = dictionary.GetValue(RedmineKeys.NAME); - version.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - version.UpdatedOn = dictionary.GetValue(RedmineKeys.UPDATED_ON); - version.DueDate = dictionary.GetValue(RedmineKeys.DUE_DATE); - version.Project = dictionary.GetValueAsIdentifiableName(RedmineKeys.PROJECT); - version.Sharing = dictionary.GetValue(RedmineKeys.SHARING); - version.Status = dictionary.GetValue(RedmineKeys.STATUS); - version.CustomFields = dictionary.GetValueAsCollection(RedmineKeys.CUSTOM_FIELDS); - - return version; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Version; - - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.NAME, entity.Name); - result.Add(RedmineKeys.STATUS, entity.Status.ToString().ToLowerInvariant()); - result.Add(RedmineKeys.SHARING, entity.Sharing.ToString().ToLowerInvariant()); - result.Add(RedmineKeys.DESCRIPTION, entity.Description); - - var root = new Dictionary(); - result.WriteDateOrEmpty(entity.DueDate, RedmineKeys.DUE_DATE); - root[RedmineKeys.VERSION] = result; - return root; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(Version)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/WatcherConverter.cs b/redmine-net40-api/JSonConverters/WatcherConverter.cs deleted file mode 100755 index 0c5b85d7..00000000 --- a/redmine-net40-api/JSonConverters/WatcherConverter.cs +++ /dev/null @@ -1,84 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class WatcherConverter : JavaScriptConverter - { - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(Watcher)}); } - } - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var watcher = new Watcher(); - - watcher.Id = dictionary.GetValue(RedmineKeys.ID); - watcher.Name = dictionary.GetValue(RedmineKeys.NAME); - - return watcher; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Watcher; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.ID, entity.Id); - } - - return result; - } - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters/WikiPageConverter.cs b/redmine-net40-api/JSonConverters/WikiPageConverter.cs deleted file mode 100644 index 6ab11bcc..00000000 --- a/redmine-net40-api/JSonConverters/WikiPageConverter.cs +++ /dev/null @@ -1,98 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class WikiPageConverter : JavaScriptConverter - { - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] { typeof(WikiPage) }); } - } - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var tracker = new WikiPage(); - - tracker.Id = dictionary.GetValue(RedmineKeys.ID); - tracker.Author = dictionary.GetValueAsIdentifiableName(RedmineKeys.AUTHOR); - tracker.Comments = dictionary.GetValue(RedmineKeys.COMMENTS); - tracker.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - tracker.Text = dictionary.GetValue(RedmineKeys.TEXT); - tracker.Title = dictionary.GetValue(RedmineKeys.TITLE); - tracker.UpdatedOn = dictionary.GetValue(RedmineKeys.UPDATED_ON); - tracker.Version = dictionary.GetValue(RedmineKeys.VERSION); - tracker.Attachments = dictionary.GetValueAsCollection(RedmineKeys.ATTACHMENTS); - - return tracker; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the objectοΏ½s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as WikiPage; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.TEXT, entity.Text); - result.Add(RedmineKeys.COMMENTS, entity.Comments); - result.WriteValueOrEmpty(entity.Version, RedmineKeys.VERSION); - result.WriteArray(RedmineKeys.UPLOADS, entity.Uploads, new UploadConverter(), serializer); - - var root = new Dictionary(); - root[RedmineKeys.WIKI_PAGE] = result; - return root; - } - - return result; - } - } -} \ No newline at end of file diff --git a/redmine-net40-api/JSonConverters2/IJsonConverter.cs b/redmine-net40-api/JSonConverters2/IJsonConverter.cs deleted file mode 100755 index 3617b563..00000000 --- a/redmine-net40-api/JSonConverters2/IJsonConverter.cs +++ /dev/null @@ -1,17 +0,0 @@ -ο»Ώusing Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Redmine.Net.Api.JSonConverters2 -{ - //public interface IJsonConverter : IJsonConverter - //{ - // void Serialize(JsonWriter writer, T obj, JsonSerializer serializer); - // new T Deserialize(JObject obj, JsonSerializer serializer); - //} - - public interface IJsonConverter - { - void Serialize(JsonWriter writer, object obj, JsonSerializer serializer); - object Deserialize(JObject obj, JsonSerializer serializer); - } -} diff --git a/redmine-net40-api/JSonConverters2/IssueConverter.cs b/redmine-net40-api/JSonConverters2/IssueConverter.cs deleted file mode 100755 index 704be682..00000000 --- a/redmine-net40-api/JSonConverters2/IssueConverter.cs +++ /dev/null @@ -1,105 +0,0 @@ -ο»Ώusing System; -using System.Globalization; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters2 -{ - public class IssueConverter : IJsonConverter - { - public void Serialize(JsonWriter writer, object obj, JsonSerializer serializer) - { - if (obj == null) return; - - var issue = (Issue) obj; - - writer.WriteStartObject(); - writer.WriteValue(RedmineKeys.ISSUE); - - writer.WriteProperty(RedmineKeys.SUBJECT, issue.Subject); - writer.WriteProperty(RedmineKeys.DESCRIPTION, issue.Description); - writer.WriteProperty(RedmineKeys.NOTES, issue.Notes); - - if (issue.Id != 0) - { - writer.WriteProperty(RedmineKeys.PRIVATE_NOTES, issue.PrivateNotes); - } - - writer.WriteProperty(RedmineKeys.IS_PRIVATE, issue.IsPrivate); - - writer.WriteIdIfNotNull(RedmineKeys.PROJECT_ID, issue.Project); - writer.WriteIdIfNotNull(RedmineKeys.PRIORITY_ID, issue.Priority); - writer.WriteIdIfNotNull(RedmineKeys.STATUS_ID, issue.Status); - writer.WriteIdIfNotNull(RedmineKeys.CATEGORY_ID, issue.Category); - writer.WriteIdIfNotNull(RedmineKeys.TRACKER_ID, issue.Tracker); - writer.WriteIdIfNotNull(RedmineKeys.ASSIGNED_TO_ID, issue.AssignedTo); - writer.WriteIdIfNotNull(RedmineKeys.FIXED_VERSION_ID, issue.FixedVersion); - writer.WriteValueOrEmpty(RedmineKeys.ESTIMATED_HOURS, issue.EstimatedHours); - - writer.WriteIdOrEmpty(RedmineKeys.PARENT_ISSUE_ID, issue.ParentIssue); - writer.WriteDateOrEmpty(RedmineKeys.START_DATE, issue.StartDate); - writer.WriteDateOrEmpty(RedmineKeys.DUE_DATE, issue.DueDate); - writer.WriteDateOrEmpty(RedmineKeys.UPDATED_ON, issue.DueDate); - - if (issue.DoneRatio != null) - writer.WriteProperty(RedmineKeys.DONE_RATIO, issue.DoneRatio.Value.ToString(CultureInfo.InvariantCulture)); - - if (issue.SpentHours != null) - writer.WriteProperty(RedmineKeys.SPENT_HOURS, issue.SpentHours.Value.ToString(CultureInfo.InvariantCulture)); - - writer.WriteArray(RedmineKeys.UPLOADS, issue.Uploads, new UploadConverter(), serializer); - writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, issue.CustomFields, new IssueCustomFieldConverter(), serializer); - - writer.WriteIdsArray(RedmineKeys.WATCHER_USER_IDS, issue.Watchers); - - writer.WriteEndObject(); - } - - public object Deserialize(JObject obj, JsonSerializer serializer) - { - if (obj == null) return null; - - var issue = new Issue - { - Id = obj.Value(RedmineKeys.ID), - Description = obj.Value(RedmineKeys.DESCRIPTION), - - Project = obj.GetValueAsIdentifiableName(RedmineKeys.PROJECT), - Tracker = obj.GetValueAsIdentifiableName(RedmineKeys.TRACKER), - Status = obj.GetValueAsIdentifiableName(RedmineKeys.STATUS), - - CreatedOn = obj.Value(RedmineKeys.CREATED_ON), - UpdatedOn = obj.Value(RedmineKeys.UPDATED_ON), - ClosedOn = obj.Value(RedmineKeys.CLOSED_ON), - - Priority = obj.GetValueAsIdentifiableName(RedmineKeys.PRIORITY), - Author = obj.GetValueAsIdentifiableName(RedmineKeys.AUTHOR), - AssignedTo = obj.GetValueAsIdentifiableName(RedmineKeys.ASSIGNED_TO), - Category = obj.GetValueAsIdentifiableName(RedmineKeys.CATEGORY), - FixedVersion = obj.GetValueAsIdentifiableName(RedmineKeys.FIXED_VERSION), - - Subject = obj.Value(RedmineKeys.SUBJECT), - Notes = obj.Value(RedmineKeys.NOTES), - IsPrivate = obj.Value(RedmineKeys.IS_PRIVATE), - StartDate = obj.Value(RedmineKeys.START_DATE), - DueDate = obj.Value(RedmineKeys.DUE_DATE), - SpentHours = obj.Value(RedmineKeys.SPENT_HOURS), - DoneRatio = obj.Value(RedmineKeys.DONE_RATIO), - EstimatedHours = obj.Value(RedmineKeys.ESTIMATED_HOURS), - - ParentIssue = obj.GetValueAsIdentifiableName(RedmineKeys.PARENT), - CustomFields = obj.GetValueAsCollection(RedmineKeys.CUSTOM_FIELDS), - Attachments = obj.GetValueAsCollection(RedmineKeys.ATTACHMENTS), - Relations = obj.GetValueAsCollection(RedmineKeys.RELATIONS), - Journals = obj.GetValueAsCollection(RedmineKeys.JOURNALS), - Changesets = obj.GetValueAsCollection(RedmineKeys.CHANGESETS), - Watchers = obj.GetValueAsCollection(RedmineKeys.WATCHERS), - Children = obj.GetValueAsCollection(RedmineKeys.CHILDREN) - }; - - return issue; - } - } -} diff --git a/redmine-net40-api/JSonConverters2/IssueCustomFieldConverter.cs b/redmine-net40-api/JSonConverters2/IssueCustomFieldConverter.cs deleted file mode 100755 index d93d870a..00000000 --- a/redmine-net40-api/JSonConverters2/IssueCustomFieldConverter.cs +++ /dev/null @@ -1,69 +0,0 @@ -ο»Ώusing System.Collections; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters2 -{ - public class IssueCustomFieldConverter : IJsonConverter - { - public void Serialize(JsonWriter writer, object obj, JsonSerializer serializer) - { - if (obj == null) return; - - var item = (IssueCustomField)obj; - - if (item.Values == null) return; - - var count = item.Values.Count; - - if (count > 1) - { - writer.WriteProperty(RedmineKeys.VALUE, item.Values.Select(x => x.Info).ToArray()); - } - else - { - writer.WriteProperty(RedmineKeys.VALUE, count > 0 ? item.Values[0].Info : null); - } - } - - public object Deserialize(JObject obj, JsonSerializer serializer) - { - if (obj == null) return null; - - var customField = new IssueCustomField - { - Id = obj.Value(RedmineKeys.ID), - Name = obj.Value(RedmineKeys.NAME), - Multiple = obj.Value(RedmineKeys.MULTIPLE) - }; - - var val = obj.Value(RedmineKeys.VALUE); - var items = serializer.Deserialize(new JTokenReader(val.ToString()), typeof(List)) as List; - - customField.Values = items; - - if (items == null) return customField; - if (customField.Values == null) customField.Values = new List(); - - var list = val as ArrayList; - - if (list != null) - { - foreach (string value in list) - { - customField.Values.Add(new CustomFieldValue {Info = value}); - } - } - else - { - customField.Values.Add(new CustomFieldValue {Info = val as string}); - } - - return customField; - } - } -} diff --git a/redmine-net40-api/JSonConverters2/UploadConverter.cs b/redmine-net40-api/JSonConverters2/UploadConverter.cs deleted file mode 100755 index ea0bc540..00000000 --- a/redmine-net40-api/JSonConverters2/UploadConverter.cs +++ /dev/null @@ -1,36 +0,0 @@ -ο»Ώusing Newtonsoft.Json.Linq; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters2 -{ - public class UploadConverter : IJsonConverter - { - public void Serialize(Newtonsoft.Json.JsonWriter writer, object obj, Newtonsoft.Json.JsonSerializer serializer) - { - if (obj == null) return; - - var item = (Upload)obj; - - writer.WriteProperty(RedmineKeys.CONTENT_TYPE, item.ContentType); - writer.WriteProperty(RedmineKeys.FILENAME, item.FileName); - writer.WriteProperty(RedmineKeys.TOKEN, item.Token); - writer.WriteProperty(RedmineKeys.DESCRIPTION, item.Description); - } - - public object Deserialize(JObject obj, Newtonsoft.Json.JsonSerializer serializer) - { - if (obj == null) return null; - - var upload = new Upload - { - ContentType = obj.Value(RedmineKeys.CONTENT_TYPE), - FileName = obj.Value(RedmineKeys.FILENAME), - Token = obj.Value(RedmineKeys.TOKEN), - Description = obj.Value(RedmineKeys.DESCRIPTION) - }; - - return upload; - } - } -} diff --git a/redmine-net40-api/MimeFormat.cs b/redmine-net40-api/MimeFormat.cs deleted file mode 100755 index dc5cb605..00000000 --- a/redmine-net40-api/MimeFormat.cs +++ /dev/null @@ -1,33 +0,0 @@ -ο»Ώ/* -Copyright 2011 - 2017 Adrian Popescu. - -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. -*/ - - -namespace Redmine.Net.Api -{ - /// - /// - /// - public enum MimeFormat - { - /// - /// - Xml, - /// - /// The json - /// - Json - } -} \ No newline at end of file diff --git a/redmine-net40-api/Properties/AssemblyInfo.cs b/redmine-net40-api/Properties/AssemblyInfo.cs deleted file mode 100644 index 20c92ff7..00000000 --- a/redmine-net40-api/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -ο»Ώusing System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("redmine-net40-api")] -[assembly: AssemblyDescription("redmine-net-api is a library for communicating with a Redmine project management application.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("redmine-net40-api")] -[assembly: AssemblyCopyright("Copyright Β© Adrian Popescu 2011 - 2017")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("8b72d103-5fba-4423-9698-ad097635a743")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.4")] -[assembly: AssemblyFileVersion("1.0.4")] diff --git a/redmine-net40-api/redmine-net40-api.csproj b/redmine-net40-api/redmine-net40-api.csproj deleted file mode 100644 index ff52cf1f..00000000 --- a/redmine-net40-api/redmine-net40-api.csproj +++ /dev/null @@ -1,363 +0,0 @@ -ο»Ώ - - - - Debug - AnyCPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E} - Library - Properties - Redmine.Net.Api - redmine-net40-api - v4.0 - 512 - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\redmine-net40-api.XML - - - none - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\redmine-net40-api.XML - - - true - bin\DebugXML\ - DEBUG;TRACE - full - AnyCPU - prompt - MinimumRecommendedRules.ruleset - 4 - false - - - true - bin\DebugJSON\ - DEBUG;TRACE - full - AnyCPU - prompt - MinimumRecommendedRules.ruleset - 4 - false - - - - - - - - - - - - - - Extensions\Extensions.cs - - - Internals\HashCodeHelper.cs - - - Internals\UrlHelper.cs - - - Internals\DataHelper.cs - - - Extensions\NameValueCollectionExtensions.cs - - - HttpVerbs.cs - - - Internals\WebApiHelper.cs - - - Logging\ColorConsoleLogger.cs - - - Logging\ConsoleLogger.cs - - - Logging\ILogger.cs - - - Logging\LogEntry.cs - - - Logging\Logger.cs - - - Logging\LoggerExtensions.cs - - - Logging\LoggingEventType.cs - - - Logging\RedmineConsoleTraceListener.cs - - - Logging\TraceLogger.cs - - - RedmineKeys.cs - - - RedmineManager.cs - - - RedmineWebClient.cs - Component - - - Types\Attachment.cs - - - Types\Attachments.cs - - - Types\ChangeSet.cs - - - Types\CustomField.cs - - - Types\CustomFieldPossibleValue.cs - - - Types\CustomFieldRole.cs - - - Types\CustomFieldValue.cs - - - Types\Detail.cs - - - Types\Error.cs - - - Types\File.cs - - - Types\Group.cs - - - Types\GroupUser.cs - - - Types\Identifiable.cs - - - Types\IdentifiableName.cs - - - Types\IRedmineManager.cs - - - Types\IRedmineWebClient.cs - - - Types\Issue.cs - - - Types\IssueCategory.cs - - - Types\IssueChild.cs - - - Types\IssueCustomField.cs - - - Types\IssuePriority.cs - - - Types\IssueRelation.cs - - - Types\IssueRelationType.cs - - - Types\IssueStatus.cs - - - Types\Journal.cs - - - Types\Membership.cs - - - Types\MembershipRole.cs - - - Types\News.cs - - - Types\PaginatedObjects.cs - - - Types\Permission.cs - - - Types\Project.cs - - - Types\ProjectEnabledModule.cs - - - Types\ProjectIssueCategory.cs - - - Types\ProjectMembership.cs - - - Types\ProjectStatus.cs - - - Types\ProjectTracker.cs - - - Types\Query.cs - - - Types\Role.cs - - - Types\TimeEntry.cs - - - Types\TimeEntryActivity.cs - - - Types\Tracker.cs - - - Types\TrackerCustomField.cs - - - Types\Upload.cs - - - Types\User.cs - - - Types\UserGroup.cs - - - Types\UserStatus.cs - - - Types\Version.cs - - - Types\Watcher.cs - - - Types\WikiPage.cs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Types\IValue.cs - - - - Exceptions\NotFoundException.cs - - - Exceptions\RedmineException.cs - - - Exceptions\RedmineTimeoutException.cs - - - Exceptions\NameResolutionFailureException.cs - - - Exceptions\InternalServerErrorException.cs - - - Exceptions\UnauthorizedException.cs - - - Exceptions\ForbiddenException.cs - - - Exceptions\ConflictException.cs - - - Exceptions\NotAcceptableException.cs - - - - - - \ No newline at end of file diff --git a/redmine-net40-api/redmine-net40-api.csproj.user b/redmine-net40-api/redmine-net40-api.csproj.user deleted file mode 100755 index a4a6cdff..00000000 --- a/redmine-net40-api/redmine-net40-api.csproj.user +++ /dev/null @@ -1,6 +0,0 @@ -ο»Ώ - - - ProjectFiles - - \ No newline at end of file diff --git a/redmine-net45-api-signed/Properties/AssemblyInfo.cs b/redmine-net45-api-signed/Properties/AssemblyInfo.cs deleted file mode 100755 index 2787d537..00000000 --- a/redmine-net45-api-signed/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -ο»Ώusing System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("redmine-net45-api-signed")] -[assembly: AssemblyDescription("redmine-net-api is a library for communicating with a Redmine project management application.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("redmine-net45-api-signed")] -[assembly: AssemblyCopyright("Copyright Β© Adrian Popescu 2011 - 2017")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("1d1fb5e7-61a9-4ac5-9a84-5714f08e09cb")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.4")] -[assembly: AssemblyFileVersion("1.0.4")] diff --git a/redmine-net45-api-signed/redmine-net-api.snk b/redmine-net45-api-signed/redmine-net-api.snk deleted file mode 100755 index 6d40dc4b..00000000 Binary files a/redmine-net45-api-signed/redmine-net-api.snk and /dev/null differ diff --git a/redmine-net45-api-signed/redmine-net45-api-signed.csproj b/redmine-net45-api-signed/redmine-net45-api-signed.csproj deleted file mode 100644 index b1d41cb3..00000000 --- a/redmine-net45-api-signed/redmine-net45-api-signed.csproj +++ /dev/null @@ -1,483 +0,0 @@ -ο»Ώ - - - - Debug - AnyCPU - {82796546-0F57-425B-BB77-751FA24D49D5} - Library - Properties - Redmine.Net.Api - redmine-net45-api-signed - v4.5 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\redmine-net45-api-signed.XML - - - none - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\redmine-net45-api-signed.XML - - - true - - - redmine-net-api.snk - - - true - bin\DebugXML\ - DEBUG;TRACE - full - AnyCPU - prompt - MinimumRecommendedRules.ruleset - 4 - false - - - true - bin\DebugJSON\ - DEBUG;TRACE - full - AnyCPU - prompt - MinimumRecommendedRules.ruleset - 4 - false - - - - - - - - - - - - - - - Internals\HashCodeHelper.cs - - - Internals\DataHelper.cs - - - Extensions\NameValueCollectionExtensions.cs - - - HttpVerbs.cs - - - Internals\UrlHelper.cs - - - Extensions\XmlWriterExtensions.cs - - - Internals\WebApiHelper.cs - - - Logging\ColorConsoleLogger.cs - - - Logging\ConsoleLogger.cs - - - Logging\ILogger.cs - - - Logging\LogEntry.cs - - - Logging\Logger.cs - - - Logging\LoggerExtensions.cs - - - Logging\LoggingEventType.cs - - - Logging\RedmineConsoleTraceListener.cs - - - Logging\TraceLogger.cs - - - RedmineKeys.cs - - - RedmineManager.cs - - - RedmineWebClient.cs - Component - - - Types\Attachment.cs - - - Types\Attachments.cs - - - Types\ChangeSet.cs - - - Types\CustomField.cs - - - Types\CustomFieldPossibleValue.cs - - - Types\CustomFieldRole.cs - - - Types\CustomFieldValue.cs - - - Types\Detail.cs - - - Types\Error.cs - - - Types\File.cs - - - - Types\Group.cs - - - Types\GroupUser.cs - - - Types\Identifiable.cs - - - Types\IdentifiableName.cs - - - Types\IRedmineManager.cs - - - Types\IRedmineWebClient.cs - - - Types\Issue.cs - - - Types\IssueCategory.cs - - - Types\IssueChild.cs - - - Types\IssueCustomField.cs - - - Types\IssuePriority.cs - - - Types\IssueRelation.cs - - - Types\IssueRelationType.cs - - - Types\IssueStatus.cs - - - Types\IValue.cs - - - Types\Journal.cs - - - Types\Membership.cs - - - Types\MembershipRole.cs - - - Types\News.cs - - - Types\PaginatedObjects.cs - - - Types\Permission.cs - - - Types\Project.cs - - - Types\ProjectEnabledModule.cs - - - Types\ProjectIssueCategory.cs - - - Types\ProjectMembership.cs - - - Types\ProjectStatus.cs - - - Types\ProjectTracker.cs - - - Types\Query.cs - - - Types\Role.cs - - - Types\TimeEntry.cs - - - Types\TimeEntryActivity.cs - - - Types\Tracker.cs - - - Types\TrackerCustomField.cs - - - Types\Upload.cs - - - Types\User.cs - - - Types\UserGroup.cs - - - Types\UserStatus.cs - - - Types\Version.cs - - - Types\Watcher.cs - - - Types\WikiPage.cs - - - Extensions\CollectionExtensions.cs - - - Extensions\JsonExtensions.cs - - - Extensions\WebExtensions.cs - - - Extensions\XmlReaderExtensions.cs - - - Internals\RedmineSerializer.cs - - - Internals\RedmineSerializerJson.cs - - - JsonConverters\AttachmentConverter.cs - - - JsonConverters\AttachmentsConverter.cs - - - JsonConverters\ChangeSetConverter.cs - - - JsonConverters\CustomFieldConverter.cs - - - JsonConverters\CustomFieldPossibleValueConverter.cs - - - JsonConverters\CustomFieldRoleConverter.cs - - - JsonConverters\DetailConverter.cs - - - JsonConverters\ErrorConverter.cs - - - JsonConverters\FileConverter.cs - - - JsonConverters\GroupConverter.cs - - - JsonConverters\GroupUserConverter.cs - - - JsonConverters\IdentifiableNameConverter.cs - - - JsonConverters\IssueCategoryConverter.cs - - - JsonConverters\IssueChildConverter.cs - - - JsonConverters\IssueConverter.cs - - - JsonConverters\IssueCustomFieldConverter.cs - - - JsonConverters\IssuePriorityConverter.cs - - - JsonConverters\IssueRelationConverter.cs - - - JsonConverters\IssueStatusConverter.cs - - - JsonConverters\JournalConverter.cs - - - JsonConverters\MembershipConverter.cs - - - JsonConverters\MembershipRoleConverter.cs - - - JsonConverters\NewsConverter.cs - - - JsonConverters\PermissionConverter.cs - - - JsonConverters\ProjectConverter.cs - - - JsonConverters\ProjectEnabledModuleConverter.cs - - - JsonConverters\ProjectIssueCategoryConverter.cs - - - JsonConverters\ProjectMembershipConverter.cs - - - JsonConverters\ProjectTrackerConverter.cs - - - JsonConverters\QueryConverter.cs - - - JsonConverters\RoleConverter.cs - - - JsonConverters\TimeEntryActivityConverter.cs - - - JsonConverters\TimeEntryConverter.cs - - - JsonConverters\TrackerConverter.cs - - - JsonConverters\TrackerCustomFieldConverter.cs - - - JsonConverters\UploadConverter.cs - - - JsonConverters\UserConverter.cs - - - JsonConverters\UserGroupConverter.cs - - - JsonConverters\VersionConverter.cs - - - JsonConverters\WatcherConverter.cs - - - JsonConverters\WikiPageConverter.cs - - - MimeFormat.cs - - - Async\RedmineManagerAsync.cs - - - Extensions\DisposableExtension.cs - - - Extensions\FunctionalExtensions.cs - - - Extensions\TaskExtensions.cs - - - Internals\WebApiAsyncHelper.cs - - - - Exceptions\NotFoundException.cs - - - Exceptions\RedmineException.cs - - - Exceptions\RedmineTimeoutException.cs - - - Exceptions\NameResolutionFailureException.cs - - - InternalServerErrorException.cs - - - Exceptions\UnauthorizedException.cs - - - Exceptions\ForbiddenException.cs - - - Exceptions\ConflictException.cs - - - Exceptions\NotAcceptableException.cs - - - - - - - - - \ No newline at end of file diff --git a/redmine-net45-api/Async/RedmineManagerAsync.cs b/redmine-net45-api/Async/RedmineManagerAsync.cs deleted file mode 100755 index a519fb51..00000000 --- a/redmine-net45-api/Async/RedmineManagerAsync.cs +++ /dev/null @@ -1,334 +0,0 @@ -ο»Ώ/* -Copyright 2011 - 2017 Adrian Popescu. - -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. -*/ - -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; -using System.Net; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.Async -{ - /// - /// - public static class RedmineManagerAsync - { - /// - /// Gets the current user asynchronous. - /// - /// The redmine manager. - /// The parameters. - /// - public static async Task GetCurrentUserAsync(this RedmineManager redmineManager, NameValueCollection parameters = null) - { - var uri = UrlHelper.GetCurrentUserUrl(redmineManager); - return await WebApiAsyncHelper.ExecuteDownload(redmineManager, uri, "GetCurrentUserAsync", parameters); - } - - /// - /// Creates the or update wiki page asynchronous. - /// - /// The redmine manager. - /// The project identifier. - /// Name of the page. - /// The wiki page. - /// - public static async Task CreateOrUpdateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage) - { - var uri = UrlHelper.GetWikiCreateOrUpdaterUrl(redmineManager, projectId, pageName); - var data = RedmineSerializer.Serialize(wikiPage, redmineManager.MimeFormat); - - return await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.PUT, data, "CreateOrUpdateWikiPageAsync"); - } - - /// - /// Deletes the wiki page asynchronous. - /// - /// The redmine manager. - /// The project identifier. - /// Name of the page. - /// - public static async Task DeleteWikiPageAsync(this RedmineManager redmineManager, string projectId, - string pageName) - { - var uri = UrlHelper.GetDeleteWikirUrl(redmineManager, projectId, pageName); - await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.DELETE, string.Empty, "DeleteWikiPageAsync"); - } - - /// - /// Support for adding attachments through the REST API is added in Redmine 1.4.0. - /// Upload a file to server. This method does not block the calling thread. - /// - /// The redmine manager. - /// The content of the file that will be uploaded on server. - /// - /// . - /// - public static async Task UploadFileAsync(this RedmineManager redmineManager, byte[] data) - { - var uri = UrlHelper.GetUploadFileUrl(redmineManager); - return await WebApiAsyncHelper.ExecuteUploadFile(redmineManager, uri, data, "UploadFileAsync"); - } - - /// - /// Downloads the file asynchronous. - /// - /// The redmine manager. - /// The address. - /// - public static async Task DownloadFileAsync(this RedmineManager redmineManager, string address) - { - return await WebApiAsyncHelper.ExecuteDownloadFile(redmineManager, address, "DownloadFileAsync"); - } - - /// - /// Gets the wiki page asynchronous. - /// - /// The redmine manager. - /// The project identifier. - /// The parameters. - /// Name of the page. - /// The version. - /// - public static async Task GetWikiPageAsync(this RedmineManager redmineManager, string projectId, - NameValueCollection parameters, string pageName, uint version = 0) - { - var uri = UrlHelper.GetWikiPageUrl(redmineManager, projectId, parameters, pageName, version); - return await WebApiAsyncHelper.ExecuteDownload(redmineManager, uri, "GetWikiPageAsync", parameters); - } - - /// - /// Gets all wiki pages asynchronous. - /// - /// The redmine manager. - /// The parameters. - /// The project identifier. - /// - public static async Task> GetAllWikiPagesAsync(this RedmineManager redmineManager, NameValueCollection parameters, string projectId) - { - var uri = UrlHelper.GetWikisUrl(redmineManager, projectId); - return await WebApiAsyncHelper.ExecuteDownloadList(redmineManager, uri, "GetAllWikiPagesAsync", parameters); - } - - /// - /// Adds an existing user to a group. This method does not block the calling thread. - /// - /// The redmine manager. - /// The group id. - /// The user id. - /// - /// Returns the Guid associated with the async request. - /// - public static async Task AddUserToGroupAsync(this RedmineManager redmineManager, int groupId, int userId) - { - var data = DataHelper.UserData(userId, redmineManager.MimeFormat); - var uri = UrlHelper.GetAddUserToGroupUrl(redmineManager, groupId); - - await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.POST, data, "AddUserToGroupAsync"); - } - - /// - /// Removes an user from a group. This method does not block the calling thread. - /// - /// The redmine manager. - /// The group id. - /// The user id. - /// - public static async Task DeleteUserFromGroupAsync(this RedmineManager redmineManager, int groupId, int userId) - { - var uri = UrlHelper.GetRemoveUserFromGroupUrl(redmineManager, groupId, userId); - await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.DELETE, string.Empty, "DeleteUserFromGroupAsync"); - } - - /// - /// Adds the watcher asynchronous. - /// - /// The redmine manager. - /// The issue identifier. - /// The user identifier. - /// - public static async Task AddWatcherAsync(this RedmineManager redmineManager, int issueId, int userId) - { - var data = DataHelper.UserData(userId, redmineManager.MimeFormat); - var uri = UrlHelper.GetAddWatcherUrl(redmineManager, issueId, userId); - - await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.POST, data, "AddWatcherAsync"); - } - - /// - /// Removes the watcher asynchronous. - /// - /// The redmine manager. - /// The issue identifier. - /// The user identifier. - /// - public static async Task RemoveWatcherAsync(this RedmineManager redmineManager, int issueId, int userId) - { - var uri = UrlHelper.GetRemoveWatcherUrl(redmineManager, issueId, userId); - await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.DELETE, string.Empty, "RemoveWatcherAsync"); - } - - /// - /// Gets the paginated objects asynchronous. - /// - /// - /// The redmine manager. - /// The parameters. - /// - public static async Task> GetPaginatedObjectsAsync(this RedmineManager redmineManager, - NameValueCollection parameters) - where T : class, new() - { - var uri = UrlHelper.GetListUrl(redmineManager, parameters); - return await WebApiAsyncHelper.ExecuteDownloadPaginatedList(redmineManager, uri, "GetPaginatedObjectsAsync", parameters); - } - - /// - /// Gets the objects asynchronous. - /// - /// - /// The redmine manager. - /// The parameters. - /// - public static async Task> GetObjectsAsync(this RedmineManager redmineManager, NameValueCollection parameters) - where T : class, new() - { - int totalCount = 0, pageSize, offset; - List resultList = null; - - if (parameters == null) parameters = new NameValueCollection(); - - int.TryParse(parameters[RedmineKeys.LIMIT], out pageSize); - int.TryParse(parameters[RedmineKeys.OFFSET], out offset); - if (pageSize == default(int)) - { - pageSize = redmineManager.PageSize > 0 - ? redmineManager.PageSize - : RedmineManager.DEFAULT_PAGE_SIZE_VALUE; - parameters.Set(RedmineKeys.LIMIT, pageSize.ToString(CultureInfo.InvariantCulture)); - } - try - { - do - { - parameters.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); - var tempResult = await redmineManager.GetPaginatedObjectsAsync(parameters); - if (tempResult != null) - { - if (resultList == null) - { - resultList = tempResult.Objects; - totalCount = tempResult.TotalCount; - } - else - { - resultList.AddRange(tempResult.Objects); - } - } - offset += pageSize; - } while (offset < totalCount); - } - catch (WebException wex) - { - wex.HandleWebException("GetObjectsAsync", redmineManager.MimeFormat); - } - return resultList; - } - - /// - /// Gets a Redmine object. This method does not block the calling thread. - /// - /// The type of objects to retrieve. - /// The redmine manager. - /// The id of the object. - /// Optional filters and/or optional fetched data. - /// - public static async Task GetObjectAsync(this RedmineManager redmineManager, string id, NameValueCollection parameters) - where T : class, new() - { - var uri = UrlHelper.GetGetUrl(redmineManager, id); - return await WebApiAsyncHelper.ExecuteDownload(redmineManager, uri, "GetobjectAsync", parameters); - } - - /// - /// Creates a new Redmine object. This method does not block the calling thread. - /// - /// The type of object to create. - /// The redmine manager. - /// The object to create. - /// - public static async Task CreateObjectAsync(this RedmineManager redmineManager, T obj) - where T : class, new() - { - return await CreateObjectAsync(redmineManager, obj, null); - } - - /// - /// Creates a new Redmine object. This method does not block the calling thread. - /// - /// The type of object to create. - /// The redmine manager. - /// The object to create. - /// The owner identifier. - /// - public static async Task CreateObjectAsync(this RedmineManager redmineManager, T obj, string ownerId) - where T : class, new() - { - var uri = UrlHelper.GetCreateUrl(redmineManager, ownerId); - var data = RedmineSerializer.Serialize(obj, redmineManager.MimeFormat); - - return await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.POST, data, "CreateObjectAsync"); - } - - /// - /// Updates the object asynchronous. - /// - /// - /// The redmine manager. - /// The identifier. - /// The object. - /// The project identifier. - /// - public static async Task UpdateObjectAsync(this RedmineManager redmineManager, string id, T obj, string projectId = null) - where T : class, new() - { - var uri = UrlHelper.GetUploadUrl(redmineManager, id, obj, projectId); - var data = RedmineSerializer.Serialize(obj, redmineManager.MimeFormat); - data = Regex.Replace(data, @"\r\n|\r|\n", "\r\n"); - - await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.PUT, data, "UpdateObjectAsync"); - } - - /// - /// Deletes the Redmine object. This method does not block the calling thread. - /// - /// The type of objects to delete. - /// The redmine manager. - /// The id of the object to delete - /// Optional filters and/or optional fetched data. - /// - public static async Task DeleteObjectAsync(this RedmineManager redmineManager, string id, NameValueCollection parameters) - where T : class, new() - { - var uri = UrlHelper.GetDeleteUrl(redmineManager, id); - await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.DELETE, string.Empty, "DeleteObjectAsync"); - } - } -} \ No newline at end of file diff --git a/redmine-net45-api/Extensions/DisposableExtension.cs b/redmine-net45-api/Extensions/DisposableExtension.cs deleted file mode 100644 index 844d9e02..00000000 --- a/redmine-net45-api/Extensions/DisposableExtension.cs +++ /dev/null @@ -1,40 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; - -namespace Redmine.Net.Api.Extensions -{ - /// - /// - /// - public static class DisposableExtension - { - /// - /// Usings the specified resource factory. - /// - /// The type of the resource. - /// The type of the result. - /// The resource factory. - /// The function. - /// - public static TResult Using(Func resourceFactory, Func fn) - where TResource : IDisposable - { - using (var resource = resourceFactory()) return fn(resource); - } - } -} \ No newline at end of file diff --git a/redmine-net45-api/Extensions/FunctionalExtensions.cs b/redmine-net45-api/Extensions/FunctionalExtensions.cs deleted file mode 100644 index 0134968f..00000000 --- a/redmine-net45-api/Extensions/FunctionalExtensions.cs +++ /dev/null @@ -1,105 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; - -namespace Redmine.Net.Api.Extensions -{ - /// - /// - /// - public static class FunctionalExtensions - { - - /// - /// The Tee extension method takes itοΏ½s name from the corresponding UNIX command which is used in command pipelines to cause a side-effect with a given input and return the original value. - /// - /// This. - /// Action. - /// The 1st type parameter. - public static T Tee(this T @this, Action action) - { - action(@this); - return @this; - } - - /// - /// Maps the specified function. - /// - /// The type of the source. - /// The type of the result. - /// The this. - /// The function. - /// - public static TResult Map(this TSource @this, Func fn) - { - return fn(@this); - } - - /// - /// Curries the specified function. - /// - /// - /// - /// - /// The function. - /// - public static Func> Curry(this Func func) - { - return a => b => func(a, b); - } - - /// - /// Curries the specified function. - /// - /// - /// - /// - /// - /// The function. - /// - public static Func>> Curry(this Func func) - { - return a => b => c => func(a, b, c); - } - - /// - /// Caches the specified function. - /// - /// - /// The function. - /// The interval. - /// - public static Func Cache(Func func, int interval) - { - var cachedValue = func(); - var timeCached= DateTime.Now; - - Func cachedFunc = () => - { - if((DateTime.Now - timeCached).Seconds >= interval) - { - timeCached = DateTime.Now; - cachedValue = func(); - } - - return cachedValue; - }; - - return cachedFunc; - } - } -} \ No newline at end of file diff --git a/redmine-net45-api/Extensions/TaskExtensions.cs b/redmine-net45-api/Extensions/TaskExtensions.cs deleted file mode 100755 index 76961369..00000000 --- a/redmine-net45-api/Extensions/TaskExtensions.cs +++ /dev/null @@ -1,136 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Threading.Tasks; - -namespace Redmine.Net.Api.Extensions -{ - - /// - /// - /// - public static class TaskExtensions - { - - /// - /// Maps the asynchronous. - /// - /// The type of the source. - /// The type of the result. - /// The this. - /// The function. - /// - public static async Task MapAsync(this Task @this, Func> fn) - { - return await fn(await @this); - } - - /// - /// Maps the asynchronous. - /// - /// The type of the source. - /// The type of the result. - /// The this. - /// The function. - /// - public static async Task MapAsync(this TSource @this, Func> fn) - { - return await fn(@this); - } - - /// - /// Maps the asynchronous. - /// - /// The type of the source. - /// The type of the result. - /// The this. - /// The function. - /// - public static async Task MapAsync(this Task @this, Func fn) - { - return fn(await @this); - } - - /// - /// Tees the asynchronous. - /// - /// The type of the source. - /// The type of the result. - /// The this. - /// The act. - /// - public static async Task TeeAsync(this TSource @this, Func> act) - { - await act(@this); - return @this; - } - - /// - /// Tees the asynchronous. - /// - /// - /// The this. - /// The act. - /// - public static async Task TeeAsync(this Task @this, Action act) - { - act(await @this); - return await @this; - } - - /// - /// Tees the asynchronous. - /// - /// The type of the source. - /// The type of the result. - /// The this. - /// The act. - /// - public static async Task TeeAsync(this Task @this, Func> act) - { - await act(await @this); - return await @this; - } - - /// - /// Tees the asynchronous. - /// - /// The type of the source. - /// The this. - /// The act. - /// - public static async Task TeeAsync(this TSource @this, Func> act) - { - await act(@this); - return @this; - } - - /// - /// Tees the specified act. - /// - /// The type of the source. - /// The type of the result. - /// The this. - /// The act. - /// - public static async Task Tee(this Task @this, Func act) - { - act(await @this); - return await @this; - } - } -} \ No newline at end of file diff --git a/redmine-net45-api/Internals/WebApiAsyncHelper.cs b/redmine-net45-api/Internals/WebApiAsyncHelper.cs deleted file mode 100644 index 33d40d84..00000000 --- a/redmine-net45-api/Internals/WebApiAsyncHelper.cs +++ /dev/null @@ -1,230 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Net; -using System.Text; -using System.Threading.Tasks; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.Internals -{ - /// - /// - /// - internal static class WebApiAsyncHelper - { - /// - /// Executes the upload. - /// - /// The redmine manager. - /// The address. - /// Type of the action. - /// The data. - /// Name of the method. - /// - public static async Task ExecuteUpload(RedmineManager redmineManager, string address, string actionType, string data, - string methodName) - { - using (var wc = redmineManager.CreateWebClient(null)) - { - try - { - if (actionType == HttpVerbs.POST || actionType == HttpVerbs.DELETE || actionType == HttpVerbs.PUT || - actionType == HttpVerbs.PATCH) - { - await wc.UploadStringTaskAsync(address, actionType, data).ConfigureAwait(false); - } - } - catch (WebException webException) - { - webException.HandleWebException(methodName, redmineManager.MimeFormat); - } - } - } - - /// - /// Executes the upload. - /// - /// - /// The redmine manager. - /// The address. - /// Type of the action. - /// The data. - /// Name of the method. - /// - public static async Task ExecuteUpload(RedmineManager redmineManager, string address, string actionType, string data, - string methodName) - where T : class, new() - { - using (var wc = redmineManager.CreateWebClient(null)) - { - try - { - if (actionType == HttpVerbs.POST || actionType == HttpVerbs.DELETE || actionType == HttpVerbs.PUT || - actionType == HttpVerbs.PATCH) - { - var response = await wc.UploadStringTaskAsync(address, actionType, data).ConfigureAwait(false); - return RedmineSerializer.Deserialize(response, redmineManager.MimeFormat); - } - } - catch (WebException webException) - { - webException.HandleWebException(methodName, redmineManager.MimeFormat); - } - return default(T); - } - } - - /// - /// Executes the download. - /// - /// - /// The redmine manager. - /// The address. - /// Name of the method. - /// The parameters. - /// - public static async Task ExecuteDownload(RedmineManager redmineManager, string address, string methodName, - NameValueCollection parameters = null) - where T : class, new() - { - using (var wc = redmineManager.CreateWebClient(parameters)) - { - try - { - var response = await wc.DownloadStringTaskAsync(address).ConfigureAwait(false); - return RedmineSerializer.Deserialize(response, redmineManager.MimeFormat); - } - catch (WebException webException) - { - webException.HandleWebException(methodName, redmineManager.MimeFormat); - } - return default(T); - } - } - - /// - /// Executes the download list. - /// - /// - /// The redmine manager. - /// The address. - /// Name of the method. - /// The parameters. - /// - public static async Task> ExecuteDownloadList(RedmineManager redmineManager, string address, - string methodName, - NameValueCollection parameters = null) where T : class, new() - { - using (var wc = redmineManager.CreateWebClient(parameters)) - { - try - { - var response = await wc.DownloadStringTaskAsync(address).ConfigureAwait(false); - var result = RedmineSerializer.DeserializeList(response, redmineManager.MimeFormat); - if (result != null) - return result.Objects; - } - catch (WebException webException) - { - webException.HandleWebException(methodName, redmineManager.MimeFormat); - } - return null; - } - } - - - /// - /// Executes the download paginated list. - /// - /// - /// The redmine manager. - /// The address. - /// Name of the method. - /// The parameters. - /// - public static async Task> ExecuteDownloadPaginatedList(RedmineManager redmineManager, string address, - string methodName, - NameValueCollection parameters = null) where T : class, new() - { - using (var wc = redmineManager.CreateWebClient(parameters)) - { - try - { - var response = await wc.DownloadStringTaskAsync(address).ConfigureAwait(false); - return RedmineSerializer.DeserializeList(response, redmineManager.MimeFormat); - } - catch (WebException webException) - { - webException.HandleWebException(methodName, redmineManager.MimeFormat); - } - return null; - } - } - - /// - /// Executes the download file. - /// - /// The redmine manager. - /// The address. - /// Name of the method. - /// - public static async Task ExecuteDownloadFile(RedmineManager redmineManager, string address, string methodName) - { - using (var wc = redmineManager.CreateWebClient(null, true)) - { - try - { - return await wc.DownloadDataTaskAsync(address); - } - catch (WebException webException) - { - webException.HandleWebException(methodName, redmineManager.MimeFormat); - } - return null; - } - } - - /// - /// Executes the upload file. - /// - /// The redmine manager. - /// The address. - /// The data. - /// Name of the method. - /// - public static async Task ExecuteUploadFile(RedmineManager redmineManager, string address, byte[] data, string methodName) - { - using (var wc = redmineManager.CreateWebClient(null, true)) - { - try - { - var response = await wc.UploadDataTaskAsync(address, data); - var responseString = Encoding.ASCII.GetString(response); - return RedmineSerializer.Deserialize(responseString, redmineManager.MimeFormat); - } - catch (WebException webException) - { - webException.HandleWebException(methodName, redmineManager.MimeFormat); - } - return null; - } - } - } -} \ No newline at end of file diff --git a/redmine-net45-api/Properties/AssemblyInfo.cs b/redmine-net45-api/Properties/AssemblyInfo.cs deleted file mode 100755 index 09a51c15..00000000 --- a/redmine-net45-api/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -ο»Ώusing System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("redmine-net45-api")] -[assembly: AssemblyDescription("redmine-net-api is a library for communicating with a Redmine project management application.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("redmine-net45-api")] -[assembly: AssemblyCopyright("Copyright Β© Adrian Popescu 2011 - 2017")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("310d3e49-5865-4b90-b645-dad29b388ac8")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.4")] -[assembly: AssemblyFileVersion("1.0.4")] diff --git a/redmine-net45-api/redmine-net45-api.csproj b/redmine-net45-api/redmine-net45-api.csproj deleted file mode 100644 index f2279076..00000000 --- a/redmine-net45-api/redmine-net45-api.csproj +++ /dev/null @@ -1,464 +0,0 @@ -ο»Ώ - - - - Debug - AnyCPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4} - Library - Properties - Redmine.Net.Api - redmine-net45-api - v4.5 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\redmine-net45-api.XML - - - none - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\redmine-net45-api.XML - - - true - bin\DebugXML\ - DEBUG;TRACE - full - AnyCPU - prompt - MinimumRecommendedRules.ruleset - 4 - false - - - true - bin\DebugJSON\ - DEBUG;TRACE - full - AnyCPU - prompt - MinimumRecommendedRules.ruleset - 4 - false - - - - - - - - - - - - - - - Internals\HashCodeHelper.cs - - - Internals\DataHelper.cs - - - Extensions\XmlWriterExtensions.cs - - - Extensions\NameValueCollectionExtensions.cs - - - HttpVerbs.cs - - - Internals\UrlHelper.cs - - - Internals\WebApiHelper.cs - - - Logging\ColorConsoleLogger.cs - - - Logging\ConsoleLogger.cs - - - Logging\ILogger.cs - - - Logging\LogEntry.cs - - - Logging\Logger.cs - - - Logging\LoggerExtensions.cs - - - Logging\LoggingEventType.cs - - - Logging\RedmineConsoleTraceListener.cs - - - Logging\TraceLogger.cs - - - RedmineKeys.cs - - - RedmineManager.cs - - - RedmineWebClient.cs - Component - - - Types\Attachment.cs - - - Types\Attachments.cs - - - Types\ChangeSet.cs - - - Types\CustomField.cs - - - Types\CustomFieldPossibleValue.cs - - - Types\CustomFieldRole.cs - - - Types\CustomFieldValue.cs - - - Types\Detail.cs - - - Types\Error.cs - - - Types\File.cs - - - - Types\Group.cs - - - Types\GroupUser.cs - - - Types\Identifiable.cs - - - Types\IdentifiableName.cs - - - Types\IRedmineManager.cs - - - Types\IRedmineWebClient.cs - - - Types\Issue.cs - - - Types\IssueCategory.cs - - - Types\IssueChild.cs - - - Types\IssueCustomField.cs - - - Types\IssuePriority.cs - - - Types\IssueRelation.cs - - - Types\IssueRelationType.cs - - - Types\IssueStatus.cs - - - Types\Journal.cs - - - Types\Membership.cs - - - Types\MembershipRole.cs - - - Types\News.cs - - - Types\PaginatedObjects.cs - - - Types\Permission.cs - - - Types\Project.cs - - - Types\ProjectEnabledModule.cs - - - Types\ProjectIssueCategory.cs - - - Types\ProjectMembership.cs - - - Types\ProjectStatus.cs - - - Types\ProjectTracker.cs - - - Types\Query.cs - - - Types\Role.cs - - - Types\TimeEntry.cs - - - Types\TimeEntryActivity.cs - - - Types\Tracker.cs - - - Types\TrackerCustomField.cs - - - Types\Upload.cs - - - Types\User.cs - - - Types\UserGroup.cs - - - Types\UserStatus.cs - - - Types\Version.cs - - - Types\Watcher.cs - - - Types\WikiPage.cs - - - Extensions\CollectionExtensions.cs - - - Extensions\JsonExtensions.cs - - - Extensions\WebExtensions.cs - - - Extensions\XmlReaderExtensions.cs - - - Internals\RedmineSerializer.cs - - - Internals\RedmineSerializerJson.cs - - - JSonConverters\AttachmentConverter.cs - - - JSonConverters\AttachmentsConverter.cs - - - JSonConverters\ChangeSetConverter.cs - - - JSonConverters\CustomFieldConverter.cs - - - JSonConverters\CustomFieldPossibleValueConverter.cs - - - JSonConverters\CustomFieldRoleConverter.cs - - - JSonConverters\DetailConverter.cs - - - JSonConverters\ErrorConverter.cs - - - JsonConverters\FileConverter.cs - - - JSonConverters\GroupConverter.cs - - - JSonConverters\GroupUserConverter.cs - - - JSonConverters\IdentifiableNameConverter.cs - - - JSonConverters\IssueCategoryConverter.cs - - - JSonConverters\IssueChildConverter.cs - - - JSonConverters\IssueConverter.cs - - - JSonConverters\IssueCustomFieldConverter.cs - - - JSonConverters\IssuePriorityConverter.cs - - - JSonConverters\IssueRelationConverter.cs - - - JSonConverters\IssueStatusConverter.cs - - - JSonConverters\JournalConverter.cs - - - JSonConverters\MembershipConverter.cs - - - JSonConverters\MembershipRoleConverter.cs - - - JSonConverters\NewsConverter.cs - - - JSonConverters\PermissionConverter.cs - - - JSonConverters\ProjectConverter.cs - - - JSonConverters\ProjectEnabledModuleConverter.cs - - - JSonConverters\ProjectIssueCategoryConverter.cs - - - JSonConverters\ProjectMembershipConverter.cs - - - JSonConverters\ProjectTrackerConverter.cs - - - JSonConverters\QueryConverter.cs - - - JSonConverters\RoleConverter.cs - - - JSonConverters\TimeEntryActivityConverter.cs - - - JSonConverters\TimeEntryConverter.cs - - - JSonConverters\TrackerConverter.cs - - - JSonConverters\TrackerCustomFieldConverter.cs - - - JSonConverters\UploadConverter.cs - - - JSonConverters\UserConverter.cs - - - JSonConverters\UserGroupConverter.cs - - - JSonConverters\VersionConverter.cs - - - JSonConverters\WatcherConverter.cs - - - JSonConverters\WikiPageConverter.cs - - - MimeFormat.cs - - - - - - - - - Types\IValue.cs - - - Exceptions\NotFoundException.cs - - - Exceptions\RedmineException.cs - - - Exceptions\RedmineTimeoutException.cs - - - Exceptions\NameResolutionFailureException.cs - - - Exceptions\InternalServerErrorException.cs - - - Exceptions\UnauthorizedException.cs - - - Exceptions\ForbiddenException.cs - - - Exceptions\ConflictException.cs - - - Exceptions\NotAcceptableException.cs - - - - - - \ No newline at end of file diff --git a/redmine-net451-api-signed/Properties/AssemblyInfo.cs b/redmine-net451-api-signed/Properties/AssemblyInfo.cs deleted file mode 100755 index 66f0ca76..00000000 --- a/redmine-net451-api-signed/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -ο»Ώusing System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("redmine-net451-api-signed")] -[assembly: AssemblyDescription("redmine-net-api is a library for communicating with a Redmine project management application.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("redmine-net451-api-signed")] -[assembly: AssemblyCopyright("Copyright Β© Adrian Popescu 2011 - 2017")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("42de2d03-79b1-475d-9f39-4a1acea67297")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.4.0")] -[assembly: AssemblyFileVersion("1.0.4.0")] diff --git a/redmine-net451-api-signed/redmine-net-api.snk b/redmine-net451-api-signed/redmine-net-api.snk deleted file mode 100755 index 9bc3c18f..00000000 Binary files a/redmine-net451-api-signed/redmine-net-api.snk and /dev/null differ diff --git a/redmine-net451-api-signed/redmine-net451-api-signed.csproj b/redmine-net451-api-signed/redmine-net451-api-signed.csproj deleted file mode 100644 index 10f3170c..00000000 --- a/redmine-net451-api-signed/redmine-net451-api-signed.csproj +++ /dev/null @@ -1,460 +0,0 @@ -ο»Ώ - - - - Debug - AnyCPU - {6E6E642E-F35A-4EA7-A2D7-16725156BD33} - Library - Properties - Redmine.Net.Api - redmine-net451-api-signed - v4.5.1 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\redmine-net451-api-signed.XML - - - none - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\redmine-net451-api-signed.XML - - - true - - - redmine-net-api.snk - - - - - - - - - - - - - - Internals\HashCodeHelper.cs - - - - Internals\DataHelper.cs - - - Extensions\XmlWriterExtensions.cs - - - Extensions\NameValueCollectionExtensions.cs - - - HttpVerbs.cs - - - Internals\UrlHelper.cs - - - Internals\WebApiHelper.cs - - - Logging\ColorConsoleLogger.cs - - - Logging\ConsoleLogger.cs - - - Logging\ILogger.cs - - - Logging\LogEntry.cs - - - Logging\Logger.cs - - - Logging\LoggerExtensions.cs - - - Logging\LoggingEventType.cs - - - Logging\RedmineConsoleTraceListener.cs - - - Logging\TraceLogger.cs - - - RedmineKeys.cs - - - RedmineManager.cs - - - RedmineWebClient.cs - Component - - - Types\Attachment.cs - - - Types\Attachments.cs - - - Types\ChangeSet.cs - - - Types\CustomField.cs - - - Types\CustomFieldPossibleValue.cs - - - Types\CustomFieldRole.cs - - - Types\CustomFieldValue.cs - - - Types\Detail.cs - - - Types\Error.cs - - - Types\File.cs - - - - Types\Group.cs - - - Types\GroupUser.cs - - - Types\Identifiable.cs - - - Types\IdentifiableName.cs - - - Types\IRedmineManager.cs - - - Types\IRedmineWebClient.cs - - - Types\Issue.cs - - - Types\IssueCategory.cs - - - Types\IssueChild.cs - - - Types\IssueCustomField.cs - - - Types\IssuePriority.cs - - - Types\IssueRelation.cs - - - Types\IssueRelationType.cs - - - Types\IssueStatus.cs - - - Types\IValue.cs - - - Types\Journal.cs - - - Types\Membership.cs - - - Types\MembershipRole.cs - - - Types\News.cs - - - Types\PaginatedObjects.cs - - - Types\Permission.cs - - - Types\Project.cs - - - Types\ProjectEnabledModule.cs - - - Types\ProjectIssueCategory.cs - - - Types\ProjectMembership.cs - - - Types\ProjectStatus.cs - - - Types\ProjectTracker.cs - - - Types\Query.cs - - - Types\Role.cs - - - Types\TimeEntry.cs - - - Types\TimeEntryActivity.cs - - - Types\Tracker.cs - - - Types\TrackerCustomField.cs - - - Types\Upload.cs - - - Types\User.cs - - - Types\UserGroup.cs - - - Types\UserStatus.cs - - - Types\Version.cs - - - Types\Watcher.cs - - - Types\WikiPage.cs - - - Extensions\CollectionExtensions.cs - - - Extensions\JsonExtensions.cs - - - Extensions\WebExtensions.cs - - - Extensions\XmlReaderExtensions.cs - - - Internals\RedmineSerializer.cs - - - Internals\RedmineSerializerJson.cs - - - JsonConverters\AttachmentConverter.cs - - - JsonConverters\AttachmentsConverter.cs - - - JsonConverters\ChangeSetConverter.cs - - - JsonConverters\CustomFieldConverter.cs - - - JsonConverters\CustomFieldPossibleValueConverter.cs - - - JsonConverters\CustomFieldRoleConverter.cs - - - JsonConverters\DetailConverter.cs - - - JsonConverters\ErrorConverter.cs - - - JsonConverters\FileConverter.cs - - - JsonConverters\GroupConverter.cs - - - JsonConverters\GroupUserConverter.cs - - - JsonConverters\IdentifiableNameConverter.cs - - - JsonConverters\IssueCategoryConverter.cs - - - JsonConverters\IssueChildConverter.cs - - - JsonConverters\IssueConverter.cs - - - JsonConverters\IssueCustomFieldConverter.cs - - - JsonConverters\IssuePriorityConverter.cs - - - JsonConverters\IssueRelationConverter.cs - - - JsonConverters\IssueStatusConverter.cs - - - JsonConverters\JournalConverter.cs - - - JsonConverters\MembershipConverter.cs - - - JsonConverters\MembershipRoleConverter.cs - - - JsonConverters\NewsConverter.cs - - - JsonConverters\PermissionConverter.cs - - - JsonConverters\ProjectConverter.cs - - - JsonConverters\ProjectEnabledModuleConverter.cs - - - JsonConverters\ProjectIssueCategoryConverter.cs - - - JsonConverters\ProjectMembershipConverter.cs - - - JsonConverters\ProjectTrackerConverter.cs - - - JsonConverters\QueryConverter.cs - - - JsonConverters\RoleConverter.cs - - - JsonConverters\TimeEntryActivityConverter.cs - - - JsonConverters\TimeEntryConverter.cs - - - JsonConverters\TrackerConverter.cs - - - JsonConverters\TrackerCustomFieldConverter.cs - - - JsonConverters\UploadConverter.cs - - - JsonConverters\UserConverter.cs - - - JsonConverters\UserGroupConverter.cs - - - JsonConverters\VersionConverter.cs - - - JsonConverters\WatcherConverter.cs - - - JsonConverters\WikiPageConverter.cs - - - MimeFormat.cs - - - Async\RedmineManagerAsync.cs - - - Extensions\DisposableExtension.cs - - - Extensions\FunctionalExtensions.cs - - - Extensions\TaskExtensions.cs - - - Internals\WebApiAsyncHelper.cs - - - - Exceptions\NotFoundException.cs - - - Exceptions\RedmineException.cs - - - Exceptions\RedmineTimeoutException.cs - - - Exceptions\NameResolutionFailureException.cs - - - Exceptions\InternalServerErrorException.cs - - - Exceptions\UnauthorizedException.cs - - - Exceptions\ForbiddenException.cs - - - Exceptions\ConflictException.cs - - - Exceptions\NotAcceptableException.cs - - - - - - - - \ No newline at end of file diff --git a/redmine-net451-api/Properties/AssemblyInfo.cs b/redmine-net451-api/Properties/AssemblyInfo.cs deleted file mode 100755 index e0d22000..00000000 --- a/redmine-net451-api/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -ο»Ώusing System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("redmine-net451-api")] -[assembly: AssemblyDescription("redmine-net-api is a library for communicating with a Redmine project management application.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("redmine-net451-api")] -[assembly: AssemblyCopyright("Copyright Β© Adrian Popescu 2011 - 2017")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("dc861b73-64ca-4bf8-a6ba-73db00934aa9")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.4.0")] -[assembly: AssemblyFileVersion("1.0.4.0")] diff --git a/redmine-net451-api/redmine-net451-api.csproj b/redmine-net451-api/redmine-net451-api.csproj deleted file mode 100644 index ae7b5695..00000000 --- a/redmine-net451-api/redmine-net451-api.csproj +++ /dev/null @@ -1,451 +0,0 @@ -ο»Ώ - - - - Debug - AnyCPU - {4AB94C09-8CFB-41C6-87D1-6B972E7F9307} - Library - Properties - Redmine.Net.Api - redmine-net451-api - v4.5.1 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\redmine-net451-api.XML - - - none - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\redmine-net451-api.XML - - - - - - - - - - - - - - Internals\HashCodeHelper.cs - - - - Internals\DataHelper.cs - - - Extensions\NameValueCollectionExtensions.cs - - - Extensions\XmlWriterExtensions.cs - - - HttpVerbs.cs - - - Internals\UrlHelper.cs - - - Internals\WebApiHelper.cs - - - Logging\ColorConsoleLogger.cs - - - Logging\ConsoleLogger.cs - - - Logging\ILogger.cs - - - Logging\LogEntry.cs - - - Logging\Logger.cs - - - Logging\LoggerExtensions.cs - - - Logging\LoggingEventType.cs - - - Logging\RedmineConsoleTraceListener.cs - - - Logging\TraceLogger.cs - - - RedmineKeys.cs - - - RedmineManager.cs - - - RedmineWebClient.cs - Component - - - Types\Attachment.cs - - - Types\Attachments.cs - - - Types\ChangeSet.cs - - - Types\CustomField.cs - - - Types\CustomFieldPossibleValue.cs - - - Types\CustomFieldRole.cs - - - Types\CustomFieldValue.cs - - - Types\Detail.cs - - - Types\Error.cs - - - Types\File.cs - - - - Types\Group.cs - - - Types\GroupUser.cs - - - Types\Identifiable.cs - - - Types\IdentifiableName.cs - - - Types\IRedmineManager.cs - - - Types\IRedmineWebClient.cs - - - Types\Issue.cs - - - Types\IssueCategory.cs - - - Types\IssueChild.cs - - - Types\IssueCustomField.cs - - - Types\IssuePriority.cs - - - Types\IssueRelation.cs - - - Types\IssueRelationType.cs - - - Types\IssueStatus.cs - - - Types\IValue.cs - - - Types\Journal.cs - - - Types\Membership.cs - - - Types\MembershipRole.cs - - - Types\News.cs - - - Types\PaginatedObjects.cs - - - Types\Permission.cs - - - Types\Project.cs - - - Types\ProjectEnabledModule.cs - - - Types\ProjectIssueCategory.cs - - - Types\ProjectMembership.cs - - - Types\ProjectStatus.cs - - - Types\ProjectTracker.cs - - - Types\Query.cs - - - Types\Role.cs - - - Types\TimeEntry.cs - - - Types\TimeEntryActivity.cs - - - Types\Tracker.cs - - - Types\TrackerCustomField.cs - - - Types\Upload.cs - - - Types\User.cs - - - Types\UserGroup.cs - - - Types\UserStatus.cs - - - Types\Version.cs - - - Types\Watcher.cs - - - Types\WikiPage.cs - - - Extensions\CollectionExtensions.cs - - - Extensions\JsonExtensions.cs - - - Extensions\WebExtensions.cs - - - Extensions\XmlReaderExtensions.cs - - - Internals\RedmineSerializer.cs - - - Internals\RedmineSerializerJson.cs - - - JsonConverters\AttachmentConverter.cs - - - JsonConverters\AttachmentsConverter.cs - - - JsonConverters\ChangeSetConverter.cs - - - JsonConverters\CustomFieldConverter.cs - - - JsonConverters\CustomFieldPossibleValueConverter.cs - - - JsonConverters\CustomFieldRoleConverter.cs - - - JsonConverters\DetailConverter.cs - - - JsonConverters\ErrorConverter.cs - - - JsonConverters\FileConverter.cs - - - JsonConverters\GroupConverter.cs - - - JsonConverters\GroupUserConverter.cs - - - JsonConverters\IdentifiableNameConverter.cs - - - JsonConverters\IssueCategoryConverter.cs - - - JsonConverters\IssueChildConverter.cs - - - JsonConverters\IssueConverter.cs - - - JsonConverters\IssueCustomFieldConverter.cs - - - JsonConverters\IssuePriorityConverter.cs - - - JsonConverters\IssueRelationConverter.cs - - - JsonConverters\IssueStatusConverter.cs - - - JsonConverters\JournalConverter.cs - - - JsonConverters\MembershipConverter.cs - - - JsonConverters\MembershipRoleConverter.cs - - - JsonConverters\NewsConverter.cs - - - JsonConverters\PermissionConverter.cs - - - JsonConverters\ProjectConverter.cs - - - JsonConverters\ProjectEnabledModuleConverter.cs - - - JsonConverters\ProjectIssueCategoryConverter.cs - - - JsonConverters\ProjectMembershipConverter.cs - - - JsonConverters\ProjectTrackerConverter.cs - - - JsonConverters\QueryConverter.cs - - - JsonConverters\RoleConverter.cs - - - JsonConverters\TimeEntryActivityConverter.cs - - - JsonConverters\TimeEntryConverter.cs - - - JsonConverters\TrackerConverter.cs - - - JsonConverters\TrackerCustomFieldConverter.cs - - - JsonConverters\UploadConverter.cs - - - JsonConverters\UserConverter.cs - - - JsonConverters\UserGroupConverter.cs - - - JsonConverters\VersionConverter.cs - - - JsonConverters\WatcherConverter.cs - - - JsonConverters\WikiPageConverter.cs - - - MimeFormat.cs - - - Async\RedmineManagerAsync.cs - - - Extensions\DisposableExtension.cs - - - Extensions\FunctionalExtensions.cs - - - Extensions\TaskExtensions.cs - - - Internals\WebApiAsyncHelper.cs - - - - Exceptions\NotFoundException.cs - - - Exceptions\RedmineException.cs - - - Exceptions\RedmineTimeoutException.cs - - - Exceptions\NameResolutionFailureException.cs - - - Exceptions\InternalServerErrorException.cs - - - Exceptions\UnauthorizedException.cs - - - Exceptions\ForbiddenException.cs - - - Exceptions\ConflictException.cs - - - Exceptions\NotAcceptableException.cs - - - - - \ No newline at end of file diff --git a/redmine-net452-api-signed/Internals/HashCodeHelper.cs b/redmine-net452-api-signed/Internals/HashCodeHelper.cs deleted file mode 100644 index 1cde5a51..00000000 --- a/redmine-net452-api-signed/Internals/HashCodeHelper.cs +++ /dev/null @@ -1,75 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System.Collections.Generic; - -namespace Redmine.Net.Api.Internals -{ - /// - /// - /// - internal static class HashCodeHelper - { - /// - /// Returns a hash code for the list. - /// - /// - /// The list. - /// The hash. - /// - /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. - /// - public static int GetHashCode(IList list, int hash) - { - unchecked - { - var hashCode = hash; - if (list != null) - { - hashCode = (hashCode * 13) + list.Count; - foreach (T t in list) - { - hashCode *= 13; - if (t != null) hashCode = hashCode + t.GetHashCode(); - } - } - - return hashCode; - } - } - - /// - /// Returns a hash code for this instance. - /// - /// - /// The entity. - /// The hash. - /// - /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. - /// - public static int GetHashCode(T entity, int hash) - { - unchecked - { - var hashCode = hash; - - hashCode = (hashCode * 397) ^ (entity == null ? 0 : entity.GetHashCode()); - - return hashCode; - } - } - } -} \ No newline at end of file diff --git a/redmine-net452-api-signed/Properties/AssemblyInfo.cs b/redmine-net452-api-signed/Properties/AssemblyInfo.cs deleted file mode 100644 index 38216cbb..00000000 --- a/redmine-net452-api-signed/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -ο»Ώusing System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("redmine-net452-api-signed")] -[assembly: AssemblyDescription("redmine-net-api is a library for communicating with a Redmine project management application.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("redmine-net452-api-signed")] -[assembly: AssemblyCopyright("Copyright Β© Adrian Popescu 2011 - 2017")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("0ae35da8-1d10-4fa4-8cf7-d1ec18a42fb7")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/redmine-net452-api-signed/redmine-net452-api-signed.csproj b/redmine-net452-api-signed/redmine-net452-api-signed.csproj deleted file mode 100644 index 08319187..00000000 --- a/redmine-net452-api-signed/redmine-net452-api-signed.csproj +++ /dev/null @@ -1,446 +0,0 @@ -ο»Ώ - - - - Debug - AnyCPU - {0AE35DA8-1D10-4FA4-8CF7-D1EC18A42FB7} - Library - Properties - Redmine.Net.Api - redmine-net452-api-signed - v4.5.2 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\redmine-net452-api-signed.xml - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - Internals\HashCodeHelper.cs - - - Internals\DataHelper.cs - - - Extensions\NameValueCollectionExtensions.cs - - - Extensions\XmlWriterExtensions.cs - - - HttpVerbs.cs - - - Internals\UrlHelper.cs - - - Internals\WebApiHelper.cs - - - Logging\ColorConsoleLogger.cs - - - Logging\ConsoleLogger.cs - - - Logging\ILogger.cs - - - Logging\LogEntry.cs - - - Logging\Logger.cs - - - Logging\LoggerExtensions.cs - - - Logging\LoggingEventType.cs - - - Logging\RedmineConsoleTraceListener.cs - - - Logging\TraceLogger.cs - - - RedmineKeys.cs - - - RedmineManager.cs - - - RedmineWebClient.cs - Component - - - Types\Attachment.cs - - - Types\Attachments.cs - - - Types\ChangeSet.cs - - - Types\CustomField.cs - - - Types\CustomFieldPossibleValue.cs - - - Types\CustomFieldRole.cs - - - Types\CustomFieldValue.cs - - - Types\Detail.cs - - - Types\Error.cs - - - Types\File.cs - - - Types\Group.cs - - - Types\GroupUser.cs - - - Types\Identifiable.cs - - - Types\IdentifiableName.cs - - - Types\IRedmineManager.cs - - - Types\IRedmineWebClient.cs - - - Types\Issue.cs - - - Types\IssueCategory.cs - - - Types\IssueChild.cs - - - Types\IssueCustomField.cs - - - Types\IssuePriority.cs - - - Types\IssueRelation.cs - - - Types\IssueRelationType.cs - - - Types\IssueStatus.cs - - - Types\IValue.cs - - - Types\Journal.cs - - - Types\Membership.cs - - - Types\MembershipRole.cs - - - Types\News.cs - - - Types\PaginatedObjects.cs - - - Types\Permission.cs - - - Types\Project.cs - - - Types\ProjectEnabledModule.cs - - - Types\ProjectIssueCategory.cs - - - Types\ProjectMembership.cs - - - Types\ProjectStatus.cs - - - Types\ProjectTracker.cs - - - Types\Query.cs - - - Types\Role.cs - - - Types\TimeEntry.cs - - - Types\TimeEntryActivity.cs - - - Types\Tracker.cs - - - Types\TrackerCustomField.cs - - - Types\Upload.cs - - - Types\User.cs - - - Types\UserGroup.cs - - - Types\UserStatus.cs - - - Types\Version.cs - - - Types\Watcher.cs - - - Types\WikiPage.cs - - - Extensions\CollectionExtensions.cs - - - Extensions\JsonExtensions.cs - - - Extensions\WebExtensions.cs - - - Extensions\XmlReaderExtensions.cs - - - Internals\RedmineSerializer.cs - - - Internals\RedmineSerializerJson.cs - - - JsonConverters\AttachmentConverter.cs - - - JsonConverters\AttachmentsConverter.cs - - - JsonConverters\ChangeSetConverter.cs - - - JsonConverters\CustomFieldConverter.cs - - - JsonConverters\CustomFieldPossibleValueConverter.cs - - - JsonConverters\CustomFieldRoleConverter.cs - - - JsonConverters\DetailConverter.cs - - - JsonConverters\ErrorConverter.cs - - - JsonConverters\FileConverter.cs - - - JsonConverters\GroupConverter.cs - - - JsonConverters\GroupUserConverter.cs - - - JsonConverters\IdentifiableNameConverter.cs - - - JsonConverters\IssueCategoryConverter.cs - - - JsonConverters\IssueChildConverter.cs - - - JsonConverters\IssueConverter.cs - - - JsonConverters\IssueCustomFieldConverter.cs - - - JsonConverters\IssuePriorityConverter.cs - - - JsonConverters\IssueRelationConverter.cs - - - JsonConverters\IssueStatusConverter.cs - - - JsonConverters\JournalConverter.cs - - - JsonConverters\MembershipConverter.cs - - - JsonConverters\MembershipRoleConverter.cs - - - JsonConverters\NewsConverter.cs - - - JsonConverters\PermissionConverter.cs - - - JsonConverters\ProjectConverter.cs - - - JsonConverters\ProjectEnabledModuleConverter.cs - - - JsonConverters\ProjectIssueCategoryConverter.cs - - - JsonConverters\ProjectMembershipConverter.cs - - - JsonConverters\ProjectTrackerConverter.cs - - - JsonConverters\QueryConverter.cs - - - JsonConverters\RoleConverter.cs - - - JsonConverters\TimeEntryActivityConverter.cs - - - JsonConverters\TimeEntryConverter.cs - - - JsonConverters\TrackerConverter.cs - - - JsonConverters\TrackerCustomFieldConverter.cs - - - JsonConverters\UploadConverter.cs - - - JsonConverters\UserConverter.cs - - - JsonConverters\UserGroupConverter.cs - - - JsonConverters\VersionConverter.cs - - - JsonConverters\WatcherConverter.cs - - - JsonConverters\WikiPageConverter.cs - - - MimeFormat.cs - - - Async\RedmineManagerAsync.cs - - - Extensions\DisposableExtension.cs - - - Extensions\FunctionalExtensions.cs - - - Extensions\TaskExtensions.cs - - - Internals\WebApiAsyncHelper.cs - - - - Exceptions\NotFoundException.cs - - - Exceptions\RedmineException.cs - - - Exceptions\RedmineTimeoutException.cs - - - Exceptions\NameResolutionFailureException.cs - - - Exceptions\InternalServerErrorException.cs - - - Exceptions\UnauthorizedException.cs - - - Exceptions\ForbiddenException.cs - - - Exceptions\ConflictException.cs - - - Exceptions\NotAcceptableException.cs - - - - - redmine-net-api.snk - - - - \ No newline at end of file diff --git a/redmine-net452-api/Properties/AssemblyInfo.cs b/redmine-net452-api/Properties/AssemblyInfo.cs deleted file mode 100644 index 6da8686d..00000000 --- a/redmine-net452-api/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -ο»Ώusing System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("redmine-net452-api")] -[assembly: AssemblyDescription("redmine-net-api is a library for communicating with a Redmine project management application.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("redmine-net452-api")] -[assembly: AssemblyCopyright("Copyright Β© Adrian Popescu 2011 - 2017")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("404b264f-363b-44ad-ae8d-2587c2e6fa82")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/redmine-net452-api/redmine-net452-api.csproj b/redmine-net452-api/redmine-net452-api.csproj deleted file mode 100644 index 14512eaf..00000000 --- a/redmine-net452-api/redmine-net452-api.csproj +++ /dev/null @@ -1,441 +0,0 @@ -ο»Ώ - - - - Debug - AnyCPU - {404B264F-363B-44AD-AE8D-2587C2E6FA82} - Library - Properties - Redmine.Net.Api - redmine-net452-api - v4.5.2 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\redmine-net452-api.xml - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - Internals\HashCodeHelper.cs - - - Internals\DataHelper.cs - - - Extensions\NameValueCollectionExtensions.cs - - - Extensions\XmlWriterExtensions.cs - - - HttpVerbs.cs - - - Internals\UrlHelper.cs - - - Internals\WebApiHelper.cs - - - Logging\ColorConsoleLogger.cs - - - Logging\ConsoleLogger.cs - - - Logging\ILogger.cs - - - Logging\LogEntry.cs - - - Logging\Logger.cs - - - Logging\LoggerExtensions.cs - - - Logging\LoggingEventType.cs - - - Logging\RedmineConsoleTraceListener.cs - - - Logging\TraceLogger.cs - - - RedmineKeys.cs - - - RedmineManager.cs - - - RedmineWebClient.cs - Component - - - Types\Attachment.cs - - - Types\Attachments.cs - - - Types\ChangeSet.cs - - - Types\CustomField.cs - - - Types\CustomFieldPossibleValue.cs - - - Types\CustomFieldRole.cs - - - Types\CustomFieldValue.cs - - - Types\Detail.cs - - - Types\Error.cs - - - Types\File.cs - - - Types\Group.cs - - - Types\GroupUser.cs - - - Types\Identifiable.cs - - - Types\IdentifiableName.cs - - - Types\IRedmineManager.cs - - - Types\IRedmineWebClient.cs - - - Types\Issue.cs - - - Types\IssueCategory.cs - - - Types\IssueChild.cs - - - Types\IssueCustomField.cs - - - Types\IssuePriority.cs - - - Types\IssueRelation.cs - - - Types\IssueRelationType.cs - - - Types\IssueStatus.cs - - - Types\IValue.cs - - - Types\Journal.cs - - - Types\Membership.cs - - - Types\MembershipRole.cs - - - Types\News.cs - - - Types\PaginatedObjects.cs - - - Types\Permission.cs - - - Types\Project.cs - - - Types\ProjectEnabledModule.cs - - - Types\ProjectIssueCategory.cs - - - Types\ProjectMembership.cs - - - Types\ProjectStatus.cs - - - Types\ProjectTracker.cs - - - Types\Query.cs - - - Types\Role.cs - - - Types\TimeEntry.cs - - - Types\TimeEntryActivity.cs - - - Types\Tracker.cs - - - Types\TrackerCustomField.cs - - - Types\Upload.cs - - - Types\User.cs - - - Types\UserGroup.cs - - - Types\UserStatus.cs - - - Types\Version.cs - - - Types\Watcher.cs - - - Types\WikiPage.cs - - - Extensions\CollectionExtensions.cs - - - Extensions\JsonExtensions.cs - - - Extensions\WebExtensions.cs - - - Extensions\XmlReaderExtensions.cs - - - Internals\RedmineSerializer.cs - - - Internals\RedmineSerializerJson.cs - - - JsonConverters\AttachmentConverter.cs - - - JsonConverters\AttachmentsConverter.cs - - - JsonConverters\ChangeSetConverter.cs - - - JsonConverters\CustomFieldConverter.cs - - - JsonConverters\CustomFieldPossibleValueConverter.cs - - - JsonConverters\CustomFieldRoleConverter.cs - - - JsonConverters\DetailConverter.cs - - - JsonConverters\ErrorConverter.cs - - - JsonConverters\FileConverter.cs - - - JsonConverters\GroupConverter.cs - - - JsonConverters\GroupUserConverter.cs - - - JsonConverters\IdentifiableNameConverter.cs - - - JsonConverters\IssueCategoryConverter.cs - - - JsonConverters\IssueChildConverter.cs - - - JsonConverters\IssueConverter.cs - - - JsonConverters\IssueCustomFieldConverter.cs - - - JsonConverters\IssuePriorityConverter.cs - - - JsonConverters\IssueRelationConverter.cs - - - JsonConverters\IssueStatusConverter.cs - - - JsonConverters\JournalConverter.cs - - - JsonConverters\MembershipConverter.cs - - - JsonConverters\MembershipRoleConverter.cs - - - JsonConverters\NewsConverter.cs - - - JsonConverters\PermissionConverter.cs - - - JsonConverters\ProjectConverter.cs - - - JsonConverters\ProjectEnabledModuleConverter.cs - - - JsonConverters\ProjectIssueCategoryConverter.cs - - - JsonConverters\ProjectMembershipConverter.cs - - - JsonConverters\ProjectTrackerConverter.cs - - - JsonConverters\QueryConverter.cs - - - JsonConverters\RoleConverter.cs - - - JsonConverters\TimeEntryActivityConverter.cs - - - JsonConverters\TimeEntryConverter.cs - - - JsonConverters\TrackerConverter.cs - - - JsonConverters\TrackerCustomFieldConverter.cs - - - JsonConverters\UploadConverter.cs - - - JsonConverters\UserConverter.cs - - - JsonConverters\UserGroupConverter.cs - - - JsonConverters\VersionConverter.cs - - - JsonConverters\WatcherConverter.cs - - - JsonConverters\WikiPageConverter.cs - - - MimeFormat.cs - - - Async\RedmineManagerAsync.cs - - - Extensions\DisposableExtension.cs - - - Extensions\FunctionalExtensions.cs - - - Extensions\TaskExtensions.cs - - - Internals\WebApiAsyncHelper.cs - - - - Exceptions\NotFoundException.cs - - - Exceptions\RedmineException.cs - - - Exceptions\RedmineTimeoutException.cs - - - Exceptions\NameResolutionFailureException.cs - - - Exceptions\InternalServerErrorException.cs - - - Exceptions\UnauthorizedException.cs - - - Exceptions\ForbiddenException.cs - - - Exceptions\ConflictException.cs - - - Exceptions\NotAcceptableException.cs - - - - \ No newline at end of file diff --git a/releasenotes.props b/releasenotes.props new file mode 100644 index 00000000..2538fad8 --- /dev/null +++ b/releasenotes.props @@ -0,0 +1,6 @@ + + + $(PackageReleaseNotes) + See $(PackageProjectUrl)/blob/master/CHANGELOG.md#v$(VersionPrefix.Replace('.','')) for more details. + + \ No newline at end of file diff --git a/signing.props b/signing.props new file mode 100644 index 00000000..1de15736 --- /dev/null +++ b/signing.props @@ -0,0 +1,6 @@ + + + true + ..\..\redmine-net-api.snk + + \ No newline at end of file diff --git a/redmine-net20-api/Types/Attachments.cs b/src/redmine-net-api/Authentication/IRedmineAuthentication.cs old mode 100755 new mode 100644 similarity index 62% rename from redmine-net20-api/Types/Attachments.cs rename to src/redmine-net-api/Authentication/IRedmineAuthentication.cs index e2d357b7..d14d6fcf --- a/redmine-net20-api/Types/Attachments.cs +++ b/src/redmine-net-api/Authentication/IRedmineAuthentication.cs @@ -1,5 +1,5 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. +/* + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,15 +14,27 @@ You may obtain a copy of the License at limitations under the License. */ -using System.Collections.Generic; +using System.Net; -namespace Redmine.Net.Api.Types +namespace Redmine.Net.Api.Authentication; + +/// +/// +/// +public interface IRedmineAuthentication { /// /// /// - internal class Attachments : Dictionary - { + string AuthenticationType { get; } + + /// + /// + /// + string Token { get; } - } + /// + /// + /// + ICredentials Credentials { get; } } \ No newline at end of file diff --git a/src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs b/src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs new file mode 100644 index 00000000..c1d59744 --- /dev/null +++ b/src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs @@ -0,0 +1,44 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System.Net; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.Authentication; + +/// +/// +/// +public sealed class RedmineApiKeyAuthentication: IRedmineAuthentication +{ + /// + public string AuthenticationType { get; } = RedmineAuthenticationType.ApiKey.ToText(); + + /// + public string Token { get; init; } + + /// + public ICredentials Credentials { get; init; } + + /// + /// + /// + /// + public RedmineApiKeyAuthentication(string apiKey) + { + Token = apiKey; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Authentication/RedmineAuthenticationType.cs b/src/redmine-net-api/Authentication/RedmineAuthenticationType.cs new file mode 100644 index 00000000..22c38cd2 --- /dev/null +++ b/src/redmine-net-api/Authentication/RedmineAuthenticationType.cs @@ -0,0 +1,8 @@ +namespace Redmine.Net.Api.Authentication; + +internal enum RedmineAuthenticationType +{ + NoAuthentication, + Basic, + ApiKey +} \ No newline at end of file diff --git a/src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs b/src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs new file mode 100644 index 00000000..810da00a --- /dev/null +++ b/src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs @@ -0,0 +1,52 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Net; +using System.Text; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.Authentication +{ + /// + /// + /// + public sealed class RedmineBasicAuthentication: IRedmineAuthentication + { + /// + public string AuthenticationType { get; } = RedmineAuthenticationType.Basic.ToText(); + + /// + public string Token { get; init; } + + /// + public ICredentials Credentials { get; init; } + + /// + /// + /// + /// + /// + public RedmineBasicAuthentication(string username, string password) + { + if (username == null) throw new RedmineException(nameof(username)); + if (password == null) throw new RedmineException(nameof(password)); + + Token = $"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}"))}"; + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs b/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs new file mode 100644 index 00000000..d8518828 --- /dev/null +++ b/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs @@ -0,0 +1,35 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System.Net; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.Authentication; + +/// +/// +/// +public sealed class RedmineNoAuthentication: IRedmineAuthentication +{ + /// + public string AuthenticationType { get; } = RedmineAuthenticationType.NoAuthentication.ToText(); + + /// + public string Token { get; init; } + + /// + public ICredentials Credentials { get; init; } +} \ No newline at end of file diff --git a/src/redmine-net-api/Common/AType.cs b/src/redmine-net-api/Common/AType.cs new file mode 100644 index 00000000..80d0493c --- /dev/null +++ b/src/redmine-net-api/Common/AType.cs @@ -0,0 +1,12 @@ +using System; + +namespace Redmine.Net.Api.Common; + +internal readonly struct A{ + public static A Is => default; +#pragma warning disable CS0184 // 'is' expression's given expression is never of the provided type + public static bool IsEqual() => Is is A; +#pragma warning restore CS0184 // 'is' expression's given expression is never of the provided type + public static Type Value => typeof(T); + +} \ No newline at end of file diff --git a/src/redmine-net-api/Common/ArgumentVerifier.cs b/src/redmine-net-api/Common/ArgumentVerifier.cs new file mode 100644 index 00000000..c4e7ede7 --- /dev/null +++ b/src/redmine-net-api/Common/ArgumentVerifier.cs @@ -0,0 +1,43 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Redmine.Net.Api.Common; + +/// +/// A utility class to perform argument validations. +/// +internal static class ArgumentVerifier +{ + /// + /// Throws ArgumentNullException if the argument is null. + /// + /// Argument value to check. + /// Name of Argument. + public static void ThrowIfNull([NotNull] object value, string name) + { + if (value == null) + { + throw new ArgumentNullException(name); + } + } + + /// + /// Validates string and throws: + /// ArgumentNullException if the argument is null. + /// ArgumentException if the argument is empty. + /// + /// Argument value to check. + /// Name of Argument. + public static void ThrowIfNullOrEmpty([NotNull] string value, string name) + { + if (value == null) + { + throw new ArgumentNullException(name); + } + + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentException("The value cannot be null or empty", name); + } + } +} \ No newline at end of file diff --git a/redmine-net20-api/Types/IValue.cs b/src/redmine-net-api/Common/IValue.cs similarity index 90% rename from redmine-net20-api/Types/IValue.cs rename to src/redmine-net-api/Common/IValue.cs index f5ed0a1e..d95d24eb 100755 --- a/redmine-net20-api/Types/IValue.cs +++ b/src/redmine-net-api/Common/IValue.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2017 Adrian Popescu. + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ You may obtain a copy of the License at limitations under the License. */ -namespace Redmine.Net.Api.Types +namespace Redmine.Net.Api.Common { /// /// diff --git a/src/redmine-net-api/Common/PagedResults.cs b/src/redmine-net-api/Common/PagedResults.cs new file mode 100644 index 00000000..af1e82fd --- /dev/null +++ b/src/redmine-net-api/Common/PagedResults.cs @@ -0,0 +1,80 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System.Collections.Generic; + +namespace Redmine.Net.Api.Common +{ + /// + /// + /// + public sealed class PagedResults where TOut: class + { + /// + /// + /// + /// + /// + /// + /// + public PagedResults(List items, int total, int offset, int pageSize) + { + Items = items; + TotalItems = total; + Offset = offset; + PageSize = pageSize; + + if (pageSize <= 0 || total == 0) + { + return; + } + + CurrentPage = offset / pageSize + 1; + + TotalPages = total / pageSize + 1; + } + + /// + /// + /// + public int PageSize { get; } + + /// + /// + /// + public int Offset { get; } + + /// + /// + /// + public int CurrentPage { get; } + + /// + /// + /// + public int TotalItems { get; } + + /// + /// + /// + public int TotalPages { get; } + + /// + /// + /// + public List Items { get; } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/RedmineApiException.cs b/src/redmine-net-api/Exceptions/RedmineApiException.cs new file mode 100644 index 00000000..fbe168ed --- /dev/null +++ b/src/redmine-net-api/Exceptions/RedmineApiException.cs @@ -0,0 +1,208 @@ +using System; +using System.Net; +#if NET40_OR_GREATER || NET +using System.Net.Http; +#endif +using System.Net.Sockets; +using System.Runtime.Serialization; +using Redmine.Net.Api.Http.Constants; + +namespace Redmine.Net.Api.Exceptions +{ + /// + /// + /// + [Serializable] + public class RedmineApiException : RedmineException + { + /// + /// Gets the error code parameter. + /// + /// The error code associated with the exception. + public override string ErrorCode => "REDMINE-API-002"; + + /// + /// Gets a value indicating whether gets exception is Transient and operation can be retried. + /// + /// Value indicating whether the exception is transient or not. + public bool IsTransient { get; protected set; } + + /// + /// + /// + public string Url { get; protected set; } + + /// + /// + /// + public int? HttpStatusCode { get; protected set; } + + /// + /// + /// + public RedmineApiException() + { + } + + /// + /// + /// + /// + public RedmineApiException(string message) : base(message) + { + } + + /// + /// + /// + /// + /// + public RedmineApiException(string message, Exception innerException) : base(message, innerException) + { + var transientErrorResult = IsTransientError(InnerException); + IsTransient = transientErrorResult.IsTransient; + HttpStatusCode = transientErrorResult.StatusCode; + } + + /// + /// + /// + /// + /// + /// + /// + public RedmineApiException(string message, string url, int? httpStatusCode = null, + Exception innerException = null) + : base(message, innerException) + { + Url = url; + var transientErrorResult = IsTransientError(InnerException); + IsTransient = transientErrorResult.IsTransient; + HttpStatusCode = httpStatusCode ?? transientErrorResult.StatusCode; + } + + /// + /// + /// + /// + /// + /// + /// + public RedmineApiException(string message, Uri requestUri, int? httpStatusCode = null, Exception innerException = null) + : base(message, innerException) + { + Url = requestUri?.ToString(); + var transientErrorResult = IsTransientError(InnerException); + IsTransient = transientErrorResult.IsTransient; + HttpStatusCode = httpStatusCode ?? transientErrorResult.StatusCode; + } + +#if !(NET8_0_OR_GREATER) + /// + protected RedmineApiException(SerializationInfo info, StreamingContext context) : base(info, context) + { + Url = info.GetString(nameof(Url)); + HttpStatusCode = info.GetInt32(nameof(HttpStatusCode)); + } + + /// + /// + /// + /// + /// + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + info.AddValue(nameof(Url), Url); + info.AddValue(nameof(HttpStatusCode), HttpStatusCode); + } +#endif + + internal struct TransientErrorResult(bool isTransient, int? statusCode) + { + public bool IsTransient = isTransient; + public int? StatusCode = statusCode; + } + + internal static TransientErrorResult IsTransientError(Exception exception) + { + return exception switch + { + null => new TransientErrorResult(false, null), + WebException webEx => HandleWebException(webEx), +#if NET40_OR_GREATER || NET + HttpRequestException httpRequestEx => HandleHttpRequestException(httpRequestEx), +#endif + _ => new TransientErrorResult(false, null) + }; + } + + private static TransientErrorResult HandleWebException(WebException webEx) + { + switch (webEx.Status) + { + case WebExceptionStatus.ProtocolError when webEx.Response is HttpWebResponse response: + var statusCode = (int)response.StatusCode; + return new TransientErrorResult(IsTransientStatusCode(statusCode), statusCode); + + case WebExceptionStatus.Timeout: + return new TransientErrorResult(true, HttpConstants.StatusCodes.RequestTimeout); + + case WebExceptionStatus.NameResolutionFailure: + case WebExceptionStatus.ConnectFailure: + return new TransientErrorResult(true, HttpConstants.StatusCodes.ServiceUnavailable); + + default: + return new TransientErrorResult(false, null); + } + } + + private static bool IsTransientStatusCode(int statusCode) + { + return statusCode == HttpConstants.StatusCodes.TooManyRequests || + statusCode == HttpConstants.StatusCodes.InternalServerError || + statusCode == HttpConstants.StatusCodes.BadGateway || + statusCode == HttpConstants.StatusCodes.ServiceUnavailable || + statusCode == HttpConstants.StatusCodes.GatewayTimeout; + } + +#if NET40_OR_GREATER || NET + private static TransientErrorResult HandleHttpRequestException( + HttpRequestException httpRequestEx) + { + if (httpRequestEx.InnerException is WebException innerWebEx) + { + return HandleWebException(innerWebEx); + } + + if (httpRequestEx.InnerException is SocketException socketEx) + { + switch (socketEx.SocketErrorCode) + { + case SocketError.HostNotFound: + case SocketError.HostUnreachable: + case SocketError.NetworkUnreachable: + case SocketError.ConnectionRefused: + return new TransientErrorResult(true, HttpConstants.StatusCodes.ServiceUnavailable); + + case SocketError.TimedOut: + return new TransientErrorResult(true, HttpConstants.StatusCodes.RequestTimeout); + + default: + return new TransientErrorResult(false, null); + } + } + +#if NET + if (httpRequestEx.StatusCode.HasValue) + { + var statusCode = (int)httpRequestEx.StatusCode.Value; + return new TransientErrorResult(IsTransientStatusCode(statusCode), statusCode); + } +#endif + + return new TransientErrorResult(false, null); + } +#endif + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/RedmineException.cs b/src/redmine-net-api/Exceptions/RedmineException.cs new file mode 100644 index 00000000..95bff717 --- /dev/null +++ b/src/redmine-net-api/Exceptions/RedmineException.cs @@ -0,0 +1,83 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Serialization; + +namespace Redmine.Net.Api.Exceptions +{ + /// + /// Thrown in case something went wrong in Redmine + /// + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [Serializable] + public class RedmineException : Exception + { + /// + /// + /// + public virtual string ErrorCode => "REDMINE-GEN-001"; + + /// + /// + /// + public Dictionary ErrorDetails { get; private set; } + + /// + /// + /// + public RedmineException() { } + /// + /// + /// + /// + public RedmineException(string message) : base(message) { } + /// + /// + /// + /// + /// + public RedmineException(string message, Exception innerException) : base(message, innerException) { } + +#if !(NET8_0_OR_GREATER) + /// + /// + /// + /// + /// + protected RedmineException(SerializationInfo info, StreamingContext context) : base(info, context) { } +#endif + + /// + /// + /// + /// + /// + /// + public RedmineException AddErrorDetail(string key, string value) + { + ErrorDetails ??= new Dictionary(); + + ErrorDetails[key] = value; + return this; + } + + private string DebuggerDisplay => $"[{Message}]"; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/RedmineForbiddenException.cs b/src/redmine-net-api/Exceptions/RedmineForbiddenException.cs new file mode 100644 index 00000000..33e3d53d --- /dev/null +++ b/src/redmine-net-api/Exceptions/RedmineForbiddenException.cs @@ -0,0 +1,82 @@ +ο»Ώ/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using Redmine.Net.Api.Http.Constants; + +namespace Redmine.Net.Api.Exceptions +{ + + /// + /// Represents an exception thrown when a Redmine API request is forbidden (HTTP 403). + /// + [Serializable] + public sealed class RedmineForbiddenException : RedmineApiException + { + /// + /// Gets the error code for this exception. + /// + public override string ErrorCode => "REDMINE-FORBIDDEN-005"; + + /// + /// Initializes a new instance of the class. + /// + public RedmineForbiddenException() + : base("Access to the Redmine API resource is forbidden.", (string)null, HttpConstants.StatusCodes.Forbidden, null) + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message that explains the reason for the exception. + /// Thrown when is null. + public RedmineForbiddenException(string message) + : base(message, (string)null, HttpConstants.StatusCodes.Forbidden, null) + { } + + /// + /// Initializes a new instance of the class with a specified error message and URL. + /// + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource that caused the exception. + /// Thrown when is null. + public RedmineForbiddenException(string message, string url) + : base(message, url, HttpConstants.StatusCodes.Forbidden, null) + { } + + /// + /// Initializes a new instance of the class with a specified error message and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + /// Thrown when or is null. + public RedmineForbiddenException(string message, Exception innerException) + : base(message, (string)null, HttpConstants.StatusCodes.Forbidden, innerException) + { } + + /// + /// Initializes a new instance of the class with a specified error message, URL, and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource that caused the exception. + /// The exception that is the cause of the current exception. + /// Thrown when or is null. + public RedmineForbiddenException(string message, string url, Exception innerException) + : base(message, url, HttpConstants.StatusCodes.Forbidden, innerException) + { } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/RedmineNotAcceptableException.cs b/src/redmine-net-api/Exceptions/RedmineNotAcceptableException.cs new file mode 100644 index 00000000..8fa39cc3 --- /dev/null +++ b/src/redmine-net-api/Exceptions/RedmineNotAcceptableException.cs @@ -0,0 +1,56 @@ +ο»Ώ/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; + +namespace Redmine.Net.Api.Exceptions +{ + /// + /// + /// + [Serializable] + public sealed class RedmineNotAcceptableException : RedmineApiException + { + /// + public override string ErrorCode => "REDMINE-NOT-ACCEPTABLE-010"; + + /// + /// Initializes a new instance of the class. + /// + public RedmineNotAcceptableException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + public RedmineNotAcceptableException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// + public RedmineNotAcceptableException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/RedmineNotFoundException.cs b/src/redmine-net-api/Exceptions/RedmineNotFoundException.cs new file mode 100644 index 00000000..b6a6e7bc --- /dev/null +++ b/src/redmine-net-api/Exceptions/RedmineNotFoundException.cs @@ -0,0 +1,80 @@ +ο»Ώ/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using Redmine.Net.Api.Http.Constants; + +namespace Redmine.Net.Api.Exceptions +{ + /// + /// Thrown in case the objects requested for could not be found. + /// + /// + [Serializable] + public sealed class RedmineNotFoundException : RedmineApiException + { + /// + public override string ErrorCode => "REDMINE-NOTFOUND-006"; + + /// + /// Initializes a new instance of the class. + /// + public RedmineNotFoundException() + : base("The requested Redmine API resource was not found.", (string)null, HttpConstants.StatusCodes.NotFound, null) + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message that explains the reason for the exception. + /// Thrown when is null. + public RedmineNotFoundException(string message) + : base(message, (string)null, HttpConstants.StatusCodes.NotFound, null) + { } + + /// + /// Initializes a new instance of the class with a specified error message and URL. + /// + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource that was not found. + /// Thrown when is null. + public RedmineNotFoundException(string message, string url) + : base(message, url, HttpConstants.StatusCodes.NotFound, null) + { } + + /// + /// Initializes a new instance of the class with a specified error message and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + /// Thrown when or is null. + public RedmineNotFoundException(string message, Exception innerException) + : base(message, (string)null, HttpConstants.StatusCodes.NotFound, innerException) + { } + + /// + /// Initializes a new instance of the class with a specified error message, URL, and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource that was not found. + /// The exception that is the cause of the current exception. + /// Thrown when or is null. + public RedmineNotFoundException(string message, string url, Exception innerException) + : base(message, url, HttpConstants.StatusCodes.NotFound, innerException) + { } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/RedmineOperationCanceledException.cs b/src/redmine-net-api/Exceptions/RedmineOperationCanceledException.cs new file mode 100644 index 00000000..f3dcc39e --- /dev/null +++ b/src/redmine-net-api/Exceptions/RedmineOperationCanceledException.cs @@ -0,0 +1,85 @@ +using System; +using Redmine.Net.Api.Http.Constants; + +namespace Redmine.Net.Api.Exceptions; + +/// +/// Represents an exception thrown when a Redmine API operation is canceled, typically due to a CancellationToken. +/// +[Serializable] +public sealed class RedmineOperationCanceledException : RedmineException +{ + /// + /// Gets the error code for this exception. + /// + public override string ErrorCode => "REDMINE-CANCEL-003"; + + /// + /// Gets the URL of the Redmine API resource associated with the canceled operation, if applicable. + /// + public string Url { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + public RedmineOperationCanceledException() + : base("The Redmine API operation was canceled.") + { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message that explains the reason for the exception. + /// Thrown when is null. + public RedmineOperationCanceledException(string message) + : base(message) + { } + + /// + /// Initializes a new instance of the class with a specified error message and URL. + /// + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource associated with the canceled operation. + /// Thrown when or is null. + public RedmineOperationCanceledException(string message, string url) + : base(message) + { + Url = url; + } + + /// + /// Initializes a new instance of the class with a specified error message and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + /// Thrown when or is null. + public RedmineOperationCanceledException(string message, Exception innerException) + : base(message, innerException) + { } + + /// + /// Initializes a new instance of the class with a specified error message, URL, and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource associated with the canceled operation. + /// The exception that is the cause of the current exception, or null if none. + /// Thrown when or is null. + public RedmineOperationCanceledException(string message, string url, Exception innerException) + : base(message, innerException) + { + Url = url; + } + + /// + /// Initializes a new instance of the class with a specified error message, URL, and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource associated with the canceled operation. + /// The exception that is the cause of the current exception, or null if none. + /// Thrown when or is null. + public RedmineOperationCanceledException(string message, Uri url, Exception innerException) + : base(message, innerException) + { + Url = url?.ToString(); + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/RedmineSerializationException.cs b/src/redmine-net-api/Exceptions/RedmineSerializationException.cs new file mode 100644 index 00000000..e864ef40 --- /dev/null +++ b/src/redmine-net-api/Exceptions/RedmineSerializationException.cs @@ -0,0 +1,70 @@ +using System; + +namespace Redmine.Net.Api.Exceptions; + +/// +/// Represents an exception thrown when a serialization or deserialization error occurs in the Redmine API client. +/// +[Serializable] +public sealed class RedmineSerializationException : RedmineException +{ + /// + public override string ErrorCode => "REDMINE-SERIALIZATION-008"; + + /// + /// Gets the name of the parameter that caused the serialization or deserialization error, if applicable. + /// + public string ParamName { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + public RedmineSerializationException() + : base("A serialization or deserialization error occurred in the Redmine API client.") + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message that explains the reason for the exception. + /// Thrown when is null. + public RedmineSerializationException(string message) + : base(message) + { } + + /// + /// Initializes a new instance of the class with a specified error message and parameter name. + /// + /// The error message that explains the reason for the exception. + /// The name of the parameter that caused the serialization or deserialization error. + /// Thrown when or is null. + public RedmineSerializationException(string message, string paramName) + : base(message) + { + ParamName = paramName ?? throw new ArgumentNullException(nameof(paramName)); + } + + /// + /// Initializes a new instance of the class with a specified error message and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + /// Thrown when or is null. + public RedmineSerializationException(string message, Exception innerException) + : base(message, innerException) + { } + + /// + /// Initializes a new instance of the class with a specified error message, parameter name, and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The name of the parameter that caused the serialization or deserialization error. + /// The exception that is the cause of the current exception. + /// Thrown when , , or is null. + public RedmineSerializationException(string message, string paramName, Exception innerException) + : base(message, innerException) + { + ParamName = paramName ?? throw new ArgumentNullException(nameof(paramName)); + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs b/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs new file mode 100644 index 00000000..038a44df --- /dev/null +++ b/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs @@ -0,0 +1,90 @@ +ο»Ώ/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using Redmine.Net.Api.Http.Constants; + +namespace Redmine.Net.Api.Exceptions +{ + /// + /// Represents an exception thrown when a Redmine API request times out (HTTP 408). + /// + [Serializable] + public sealed class RedmineTimeoutException : RedmineApiException + { + /// + public override string ErrorCode => "REDMINE-TIMEOUT-004"; + + /// + /// Initializes a new instance of the class. + /// + public RedmineTimeoutException() + : base("The Redmine API request timed out.", (string)null, HttpConstants.StatusCodes.RequestTimeout, null) + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message that explains the reason for the exception. + /// Thrown when is null. + public RedmineTimeoutException(string message) + : base(message, (string)null, HttpConstants.StatusCodes.RequestTimeout, null) + { } + + /// + /// Initializes a new instance of the class with a specified error message and URL. + /// + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource that timed out. + /// Thrown when or is null. + public RedmineTimeoutException(string message, string url) + : base(message, url, HttpConstants.StatusCodes.RequestTimeout, null) + { } + + /// + /// Initializes a new instance of the class with a specified error message and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + /// Thrown when or is null. + public RedmineTimeoutException(string message, Exception innerException) + : base(message, (string)null, HttpConstants.StatusCodes.RequestTimeout, innerException) + { } + + /// + /// Initializes a new instance of the class with a specified error message, URL, and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource that timed out. + /// The exception that is the cause of the current exception, or null if none. + /// Thrown when or is null. + public RedmineTimeoutException(string message, string url, Exception innerException) + : base(message, url, HttpConstants.StatusCodes.RequestTimeout, innerException) + { } + + /// + /// Initializes a new instance of the class with a specified error message, URL, and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource that timed out. + /// The exception that is the cause of the current exception, or null if none. + /// Thrown when or is null. + public RedmineTimeoutException(string message, Uri url, Exception innerException) + : base(message, url?.ToString(), HttpConstants.StatusCodes.RequestTimeout, innerException) + { } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/RedmineUnauthorizedException.cs b/src/redmine-net-api/Exceptions/RedmineUnauthorizedException.cs new file mode 100644 index 00000000..824d7727 --- /dev/null +++ b/src/redmine-net-api/Exceptions/RedmineUnauthorizedException.cs @@ -0,0 +1,79 @@ +ο»Ώ/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using Redmine.Net.Api.Http.Constants; + +namespace Redmine.Net.Api.Exceptions +{ + /// + /// Represents an exception thrown when a Redmine API request is unauthorized (HTTP 401). + /// + [Serializable] + public sealed class RedmineUnauthorizedException : RedmineApiException + { + /// + public override string ErrorCode => "REDMINE-UNAUTHORIZED-007"; + + /// + /// Initializes a new instance of the class. + /// + public RedmineUnauthorizedException() + : base("The Redmine API request is unauthorized.", (string)null, HttpConstants.StatusCodes.Unauthorized, null) + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message that explains the reason for the exception. + /// Thrown when is null. + public RedmineUnauthorizedException(string message) + : base(message, (string)null, HttpConstants.StatusCodes.Unauthorized, null) + { } + + /// + /// Initializes a new instance of the class with a specified error message and URL. + /// + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource that caused the unauthorized access. + /// Thrown when is null. + public RedmineUnauthorizedException(string message, string url) + : base(message, url, HttpConstants.StatusCodes.Unauthorized, null) + { } + + /// + /// Initializes a new instance of the class with a specified error message and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + /// Thrown when or is null. + public RedmineUnauthorizedException(string message, Exception innerException) + : base(message, (string)null, HttpConstants.StatusCodes.Unauthorized, innerException) + { } + + /// + /// Initializes a new instance of the class with a specified error message, URL, and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource that caused the unauthorized access. + /// The exception that is the cause of the current exception. + /// Thrown when or is null. + public RedmineUnauthorizedException(string message, string url, Exception innerException) + : base(message, url, HttpConstants.StatusCodes.Unauthorized, innerException) + { } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/RedmineUnprocessableEntityException.cs b/src/redmine-net-api/Exceptions/RedmineUnprocessableEntityException.cs new file mode 100644 index 00000000..c37dd063 --- /dev/null +++ b/src/redmine-net-api/Exceptions/RedmineUnprocessableEntityException.cs @@ -0,0 +1,63 @@ +using System; +using Redmine.Net.Api.Http.Constants; + +namespace Redmine.Net.Api.Exceptions; + +/// +/// Represents an exception thrown when a Redmine API request cannot be processed due to semantic errors (HTTP 422). +/// +[Serializable] +public class RedmineUnprocessableEntityException : RedmineApiException +{ + /// + /// Gets the error code for this exception. + /// + public override string ErrorCode => "REDMINE-UNPROCESSABLE-009"; + + /// + /// Initializes a new instance of the class. + /// + public RedmineUnprocessableEntityException() + : base("The Redmine API request cannot be processed due to invalid data.", (string)null, HttpConstants.StatusCodes.UnprocessableEntity, null) + { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message that explains the reason for the exception. + /// Thrown when is null. + public RedmineUnprocessableEntityException(string message) + : base(message, (string)null, HttpConstants.StatusCodes.UnprocessableEntity, null) + { } + + /// + /// Initializes a new instance of the class with a specified error message and URL. + /// + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource that caused the error. + /// Thrown when is null. + public RedmineUnprocessableEntityException(string message, string url) + : base(message, url, HttpConstants.StatusCodes.UnprocessableEntity, null) + { } + + /// + /// Initializes a new instance of the class with a specified error message and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + /// Thrown when or is null. + public RedmineUnprocessableEntityException(string message, Exception innerException) + : base(message, (string)null, HttpConstants.StatusCodes.UnprocessableEntity, innerException) + { } + + /// + /// Initializes a new instance of the class with a specified error message, URL, and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource that caused the error. + /// The exception that is the cause of the current exception. + /// Thrown when or is null. + public RedmineUnprocessableEntityException(string message, string url, Exception innerException) + : base(message, url, HttpConstants.StatusCodes.UnprocessableEntity, innerException) + { } +} \ No newline at end of file diff --git a/src/redmine-net-api/Extensions/EnumExtensions.cs b/src/redmine-net-api/Extensions/EnumExtensions.cs new file mode 100644 index 00000000..848fdc2e --- /dev/null +++ b/src/redmine-net-api/Extensions/EnumExtensions.cs @@ -0,0 +1,112 @@ +using Redmine.Net.Api.Authentication; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.Extensions; + +/// +/// Provides extension methods for enumerations used in the Redmine.Net.Api.Types namespace. +/// +public static class EnumExtensions +{ + /// + /// Converts the specified enumeration value to its lowercase invariant string representation. + /// + /// The enumeration value to be converted. + /// A string representation of the IssueRelationType enumeration value in a lowercase, or "undefined" if the value does not match a defined case. + public static string ToLowerName(this IssueRelationType @enum) + { + return @enum switch + { + IssueRelationType.Relates => "relates", + IssueRelationType.Duplicates => "duplicates", + IssueRelationType.Duplicated => "duplicated", + IssueRelationType.Blocks => "blocks", + IssueRelationType.Blocked => "blocked", + IssueRelationType.Precedes => "precedes", + IssueRelationType.Follows => "follows", + IssueRelationType.CopiedTo => "copied_to", + IssueRelationType.CopiedFrom => "copied_from", + _ => "undefined" + }; + } + + /// + /// Converts the specified VersionSharing enumeration value to its lowercase invariant string representation. + /// + /// The VersionSharing enumeration value to be converted. + /// A string representation of the VersionSharing enumeration value in a lowercase, or "undefined" if the value does not match a valid case. + public static string ToLowerName(this VersionSharing @enum) + { + return @enum switch + { + VersionSharing.Unknown => "unknown", + VersionSharing.None => "none", + VersionSharing.Descendants => "descendants", + VersionSharing.Hierarchy => "hierarchy", + VersionSharing.Tree => "tree", + VersionSharing.System => "system", + _ => "undefined" + }; + } + + /// + /// Converts the specified enumeration value to its lowercase invariant string representation. + /// + /// The enumeration value to be converted. + /// A lowercase string representation of the enumeration value, or "undefined" if the value does not match a defined case. + public static string ToLowerName(this VersionStatus @enum) + { + return @enum switch + { + VersionStatus.None => "none", + VersionStatus.Open => "open", + VersionStatus.Closed => "closed", + VersionStatus.Locked => "locked", + _ => "undefined" + }; + } + + /// + /// Converts the specified ProjectStatus enumeration value to its lowercase invariant string representation. + /// + /// The ProjectStatus enumeration value to be converted. + /// A string representation of the ProjectStatus enumeration value in a lowercase, or "undefined" if the value does not match a defined case. + public static string ToLowerName(this ProjectStatus @enum) + { + return @enum switch + { + ProjectStatus.None => "none", + ProjectStatus.Active => "active", + ProjectStatus.Archived => "archived", + ProjectStatus.Closed => "closed", + _ => "undefined" + }; + } + + /// + /// Converts the specified enumeration value to its lowercase invariant string representation. + /// + /// The enumeration value to be converted. + /// A string representation of the UserStatus enumeration value in a lowercase, or "undefined" if the value does not match a defined case. + public static string ToLowerName(this UserStatus @enum) + { + return @enum switch + { + UserStatus.StatusActive => "status_active", + UserStatus.StatusLocked => "status_locked", + UserStatus.StatusRegistered => "status_registered", + _ => "undefined" + }; + } + + internal static string ToText(this RedmineAuthenticationType @enum) + { + return @enum switch + { + RedmineAuthenticationType.NoAuthentication => "NoAuth", + RedmineAuthenticationType.Basic => "Basic", + RedmineAuthenticationType.ApiKey => "ApiKey", + _ => "undefined" + }; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Extensions/IEnumerableExtensions.cs b/src/redmine-net-api/Extensions/IEnumerableExtensions.cs new file mode 100644 index 00000000..98d9b355 --- /dev/null +++ b/src/redmine-net-api/Extensions/IEnumerableExtensions.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Redmine.Net.Api.Common; + +namespace Redmine.Net.Api.Extensions; + +/// +/// Provides extension methods for IEnumerable types. +/// +public static class IEnumerableExtensions +{ + /// + /// Converts a collection of objects into a string representation with each item separated by a comma + /// and enclosed within curly braces. + /// + /// The type of items in the collection. The type must be a reference type. + /// The collection of items to convert to a string representation. + /// + /// Returns a string containing all the items from the collection, separated by commas and + /// enclosed within curly braces. Returns null if the collection is null. + /// + internal static string Dump(this IEnumerable collection) where TIn : class + { + if (collection == null) + { + return null; + } + + var sb = new StringBuilder("{"); + + foreach (var item in collection) + { + sb.Append(item).Append(','); + } + + if (sb.Length > 1) + { + sb.Length -= 1; + } + + sb.Append('}'); + + var str = sb.ToString(); + sb.Length = 0; + + return str; + } + + /// + /// Returns the index of the first item in the sequence that satisfies the predicate. If no item satisfies the predicate, -1 is returned. + /// + /// The type of objects in the . + /// in which to search. + /// Function performed to check whether an item satisfies the condition. + /// Return the zero-based index of the first occurrence of an element that satisfies the condition, if found; otherwise, -1. + internal static int IndexOf(this IEnumerable source, Func predicate) + { + ArgumentVerifier.ThrowIfNull(predicate, nameof(predicate)); + + var index = 0; + + foreach (var item in source) + { + if (predicate(item)) + { + return index; + } + + index++; + } + + return -1; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Extensions/IdentifiableNameExtensions.cs b/src/redmine-net-api/Extensions/IdentifiableNameExtensions.cs new file mode 100644 index 00000000..b461a8e4 --- /dev/null +++ b/src/redmine-net-api/Extensions/IdentifiableNameExtensions.cs @@ -0,0 +1,84 @@ +using System; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Types; +using Version = Redmine.Net.Api.Types.Version; + +namespace Redmine.Net.Api.Extensions +{ + /// + /// + /// + public static class IdentifiableNameExtensions + { + /// + /// Converts an object of type into an object. + /// + /// The type of the entity to convert. Expected to be one of the supported Redmine entity types. + /// The entity object to be converted into an . + /// An object populated with the identifier and name of the specified entity, or null if the entity type is not supported. + public static IdentifiableName ToIdentifiableName(this T entity) where T : class + { + return entity switch + { + CustomField customField => IdentifiableName.Create(customField.Id, customField.Name), + CustomFieldRole customFieldRole => IdentifiableName.Create(customFieldRole.Id, customFieldRole.Name), + DocumentCategory documentCategory => IdentifiableName.Create(documentCategory.Id, documentCategory.Name), + Group group => IdentifiableName.Create(group.Id, group.Name), + GroupUser groupUser => IdentifiableName.Create(groupUser.Id, groupUser.Name), + Issue issue => new IdentifiableName(issue.Id, issue.Subject), + IssueAllowedStatus issueAllowedStatus => IdentifiableName.Create(issueAllowedStatus.Id, issueAllowedStatus.Name), + IssueCustomField issueCustomField => IdentifiableName.Create(issueCustomField.Id, issueCustomField.Name), + IssuePriority issuePriority => IdentifiableName.Create(issuePriority.Id, issuePriority.Name), + IssueStatus issueStatus => IdentifiableName.Create(issueStatus.Id, issueStatus.Name), + MembershipRole membershipRole => IdentifiableName.Create(membershipRole.Id, membershipRole.Name), + MyAccountCustomField myAccountCustomField => IdentifiableName.Create(myAccountCustomField.Id, myAccountCustomField.Name), + Project project => IdentifiableName.Create(project.Id, project.Name), + ProjectEnabledModule projectEnabledModule => IdentifiableName.Create(projectEnabledModule.Id, projectEnabledModule.Name), + ProjectIssueCategory projectIssueCategory => IdentifiableName.Create(projectIssueCategory.Id, projectIssueCategory.Name), + ProjectTimeEntryActivity projectTimeEntryActivity => IdentifiableName.Create(projectTimeEntryActivity.Id, projectTimeEntryActivity.Name), + ProjectTracker projectTracker => IdentifiableName.Create(projectTracker.Id, projectTracker.Name), + Query query => IdentifiableName.Create(query.Id, query.Name), + Role role => IdentifiableName.Create(role.Id, role.Name), + TimeEntryActivity timeEntryActivity => IdentifiableName.Create(timeEntryActivity.Id, timeEntryActivity.Name), + Tracker tracker => IdentifiableName.Create(tracker.Id, tracker.Name), + UserGroup userGroup => IdentifiableName.Create(userGroup.Id, userGroup.Name), + Version version => IdentifiableName.Create(version.Id, version.Name), + Watcher watcher => IdentifiableName.Create(watcher.Id, watcher.Name), + _ => null + }; + } + + + /// + /// Converts an integer value to an object. + /// + /// An integer value representing the identifier. Must be greater than zero. + /// An object with the specified identifier and a null name. + /// Thrown when the given value is less than or equal to zero. + public static IdentifiableName ToIdentifier(this int val) + { + if (val <= 0) + { + throw new ArgumentException("Value must be greater than zero", nameof(val)); + } + + return new IdentifiableName(val, null); + } + + /// + /// Converts an integer value into an object. + /// + /// The integer value representing the ID of an issue status. + /// An object initialized with the specified identifier. + /// Thrown when the specified value is less than or equal to zero. + public static IssueStatus ToIssueStatusIdentifier(this int val) + { + if (val <= 0) + { + throw new ArgumentException("Value must be greater than zero", nameof(val)); + } + + return new IssueStatus(val, null); + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Extensions/IntExtensions.cs b/src/redmine-net-api/Extensions/IntExtensions.cs new file mode 100644 index 00000000..fa0e57aa --- /dev/null +++ b/src/redmine-net-api/Extensions/IntExtensions.cs @@ -0,0 +1,45 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +namespace Redmine.Net.Api.Extensions; + +internal static class IntExtensions +{ + public static bool Between(this int val, int from, int to) + { + return val >= from && val <= to; + } + + public static bool Greater(this int val, int than) + { + return val > than; + } + + public static bool GreaterOrEqual(this int val, int than) + { + return val >= than; + } + + public static bool Lower(this int val, int than) + { + return val < than; + } + + public static bool LowerOrEqual(this int val, int than) + { + return val <= than; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Extensions/ListExtensions.cs b/src/redmine-net-api/Extensions/ListExtensions.cs new file mode 100755 index 00000000..1064b3f4 --- /dev/null +++ b/src/redmine-net-api/Extensions/ListExtensions.cs @@ -0,0 +1,77 @@ +ο»Ώ/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System.Collections.Generic; + +namespace Redmine.Net.Api.Extensions +{ + /// + /// Provides extension methods for operations on lists. + /// + public static class ListExtensions + { + /// + /// Creates a deep clone of the specified list. + /// + /// The type of elements in the list. Must implement . + /// The list to be cloned. + /// Specifies whether to reset the ID for each cloned item. + /// A new list containing cloned copies of the elements from the original list. Returns null if the original list is null. + public static List Clone(this List listToClone, bool resetId) where T : ICloneable + { + if (listToClone == null) + { + return null; + } + + var clonedList = new List(listToClone.Count); + + foreach (var item in listToClone) + { + clonedList.Add(item.Clone(resetId)); + } + return clonedList; + } + + /// + /// Compares two lists for equality based on their elements. + /// + /// The type of elements in the lists. Must be a reference type. + /// The first list to compare. + /// The second list to compare. + /// True if both lists are non-null, have the same count, and all corresponding elements are equal; otherwise, false. + public static bool Equals(this List list, List listToCompare) where T : class + { + if (list == null || listToCompare == null) + { + return false; + } + + if (list.Count != listToCompare.Count) + { + return false; + } + + var index = 0; + while (index < list.Count && list[index].Equals(listToCompare[index])) + { + index++; + } + + return index == list.Count; + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs new file mode 100644 index 00000000..71ebcf13 --- /dev/null +++ b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs @@ -0,0 +1,889 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using Redmine.Net.Api.Common; +#if !(NET20) +using System.Threading; +using System.Threading.Tasks; +#endif +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Http.Extensions; +using Redmine.Net.Api.Net.Internal; +using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.Extensions +{ + /// + /// + /// + public static class RedmineManagerExtensions + { + /// + /// Archives a project in Redmine based on the specified project identifier. + /// + /// The instance of the RedmineManager used to manage the API requests. + /// The unique identifier of the project to be archived. + /// Additional request options to include in the API call. + public static void ArchiveProject(this RedmineManager redmineManager, string projectIdentifier, + RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.ProjectArchive(projectIdentifier); + + redmineManager.ApiClient.Update(uri, string.Empty ,requestOptions); + } + + /// + /// Unarchives a project in Redmine based on the specified project identifier. + /// + /// The instance of the RedmineManager used to manage the API requests. + /// The unique identifier of the project to be unarchived. + /// Additional request options to include in the API call. + public static void UnarchiveProject(this RedmineManager redmineManager, string projectIdentifier, + RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.ProjectUnarchive(projectIdentifier); + + redmineManager.ApiClient.Update(uri, string.Empty ,requestOptions); + } + + /// + /// Reopens a previously closed project in Redmine based on the specified project identifier. + /// + /// The instance of the RedmineManager used to execute the API request. + /// The unique identifier of the project to be reopened. + /// Additional request options to include in the API call. + public static void ReopenProject(this RedmineManager redmineManager, string projectIdentifier, + RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.ProjectReopen(projectIdentifier); + + redmineManager.ApiClient.Update(uri, string.Empty ,requestOptions); + } + + /// + /// Closes a project in Redmine based on the specified project identifier. + /// + /// The instance of the RedmineManager used to manage the API requests. + /// The unique identifier of the project to be closed. + /// Additional request options to include in the API call. + public static void CloseProject(this RedmineManager redmineManager, string projectIdentifier, + RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.ProjectClose(projectIdentifier); + + redmineManager.ApiClient.Update(uri,string.Empty, requestOptions); + } + + /// + /// Adds a related issue to a project repository in Redmine based on the specified parameters. + /// + /// The instance of the RedmineManager used to manage the API requests. + /// The unique identifier of the project to which the repository belongs. + /// The unique identifier of the repository within the project. + /// The revision or commit ID to relate the issue to. + /// Additional request options to include in the API call. + public static void ProjectRepositoryAddRelatedIssue(this RedmineManager redmineManager, + string projectIdentifier, string repositoryIdentifier, string revision, + RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.ProjectRepositoryAddRelatedIssue(projectIdentifier, repositoryIdentifier, revision); + + _ = redmineManager.ApiClient.Create(uri,string.Empty, requestOptions); + } + + /// + /// Removes a related issue from the specified repository revision of a project in Redmine. + /// + /// The instance of the RedmineManager used to manage the API requests. + /// The unique identifier of the project containing the repository. + /// The unique identifier of the repository from which the related issue will be removed. + /// The specific revision of the repository to disassociate the issue from. + /// The unique identifier of the issue to be removed as related. + /// Additional request options to include in the API call. + public static void ProjectRepositoryRemoveRelatedIssue(this RedmineManager redmineManager, + string projectIdentifier, string repositoryIdentifier, string revision, string issueIdentifier, + RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.ProjectRepositoryRemoveRelatedIssue(projectIdentifier, repositoryIdentifier, revision, issueIdentifier); + + _ = redmineManager.ApiClient.Delete(uri, requestOptions); + } + + /// + /// Retrieves a paginated list of news for a specific project in Redmine. + /// + /// The instance of the RedmineManager used to manage the API requests. + /// The unique identifier of the project for which news is being retrieved. + /// Additional request options to include in the API call, if any. + /// A paginated list of news items associated with the specified project. + public static PagedResults GetProjectNews(this RedmineManager redmineManager, string projectIdentifier, + RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.ProjectNews(projectIdentifier); + + var response = redmineManager.GetPaginatedInternal(uri, requestOptions); + + return response; + } + + /// + /// Adds a news item to a project in Redmine based on the specified project identifier. + /// + /// The instance of the RedmineManager used to manage the API requests. + /// The unique identifier of the project to which the news will be added. + /// The news item to be added to the project, which must contain a valid title. + /// Additional request options to include in the API call. + /// The created news item as a response from the Redmine server. + /// Thrown when the provided news object is null or the news title is blank. + public static News AddProjectNews(this RedmineManager redmineManager, string projectIdentifier, News news, + RequestOptions requestOptions = null) + { + if (news == null) + { + throw new RedmineException("Argument news is null"); + } + + if (news.Title.IsNullOrWhiteSpace()) + { + throw new RedmineException("News title cannot be blank"); + } + + var payload = redmineManager.Serializer.Serialize(news); + + var uri = redmineManager.RedmineApiUrls.ProjectNews(projectIdentifier); + + var response = redmineManager.ApiClient.Create(uri, payload, requestOptions); + + return response.DeserializeTo(redmineManager.Serializer); + } + + /// + /// Retrieves the memberships associated with the specified project in Redmine. + /// + /// The instance of the RedmineManager used to manage the API requests. + /// The unique identifier of the project for which memberships are being retrieved. + /// Additional request options to include in the API call, such as pagination or filters. + /// Returns a paginated collection of project memberships for the specified project. + /// Thrown when the API request fails or an error occurs during execution. + public static PagedResults GetProjectMemberships(this RedmineManager redmineManager, + string projectIdentifier, RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.ProjectMemberships(projectIdentifier); + + var response = redmineManager.GetPaginatedInternal(uri, requestOptions); + + return response; + } + + /// + /// Retrieves the list of files associated with a specific project in Redmine. + /// + /// The instance of the RedmineManager used to manage the API requests. + /// The unique identifier of the project whose files are being retrieved. + /// Additional request options to include in the API call. + /// A paginated result containing the list of files associated with the project. + /// Thrown when the API request fails or returns an error response. + public static PagedResults GetProjectFiles(this RedmineManager redmineManager, string projectIdentifier, + RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.ProjectFilesFragment(projectIdentifier); + + var response = redmineManager.GetPaginatedInternal(uri, requestOptions); + + return response; + } + + /// + /// Returns the user whose credentials are used to access the API. + /// + /// The instance of the RedmineManager used to manage the API requests. + /// Additional request options to include in the API call. + /// The authenticated user as a object. + public static User GetCurrentUser(this RedmineManager redmineManager, RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.CurrentUser(); + + var response = redmineManager.ApiClient.Get(uri, requestOptions); + + return response.DeserializeTo(redmineManager.Serializer); + } + + /// + /// Retrieves the account details of the currently authenticated user. + /// + /// The instance of the RedmineManager used to perform the API call. + /// Optional configuration for the API request. + /// Returns the account details of the authenticated user as a MyAccount object. + public static MyAccount GetMyAccount(this RedmineManager redmineManager, RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.MyAccount(); + + var response = redmineManager.ApiClient.Get(uri, requestOptions); + + return response.DeserializeTo(redmineManager.Serializer); + } + + /// + /// Adds a watcher to a specific issue in Redmine using the specified issue ID and user ID. + /// + /// The instance of the RedmineManager used to manage the API requests. + /// The unique identifier of the issue to which the watcher will be added. + /// The unique identifier of the user to be added as a watcher. + /// Additional request options to include in the API call. + public static void AddWatcherToIssue(this RedmineManager redmineManager, int issueId, int userId, + RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.IssueWatcherAdd(issueId.ToInvariantString()); + + var payload = SerializationHelper.SerializeUserId(userId, redmineManager.Serializer); + + redmineManager.ApiClient.Create(uri, payload, requestOptions); + } + + /// + /// Removes a watcher from a specific issue in Redmine based on the specified issue identifier and user identifier. + /// + /// The instance of the RedmineManager used to manage the API requests. + /// The unique identifier of the issue from which the watcher will be removed. + /// The unique identifier of the user to be removed as a watcher. + /// Additional request options to include in the API call. + public static void RemoveWatcherFromIssue(this RedmineManager redmineManager, int issueId, int userId, + RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.IssueWatcherRemove(issueId.ToInvariantString(), userId.ToInvariantString()); + + redmineManager.ApiClient.Delete(uri, requestOptions); + } + + /// + /// Adds a user to a specified group in Redmine. + /// + /// The instance of the RedmineManager used to manage API requests. + /// The unique identifier of the group to which the user will be added. + /// The unique identifier of the user to be added to the group. + /// Additional request options to include in the API call. + public static void AddUserToGroup(this RedmineManager redmineManager, int groupId, int userId, + RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.GroupUserAdd(groupId.ToInvariantString()); + + var payload = SerializationHelper.SerializeUserId(userId, redmineManager.Serializer); + + redmineManager.ApiClient.Create(uri, payload, requestOptions); + } + + /// + /// Removes a user from a specified group in Redmine. + /// + /// The instance of the RedmineManager used to manage API requests. + /// The unique identifier of the group from which the user will be removed. + /// The unique identifier of the user to be removed from the group. + /// Additional request options to customize the API call. + public static void RemoveUserFromGroup(this RedmineManager redmineManager, int groupId, int userId, + RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.GroupUserRemove(groupId.ToInvariantString(), userId.ToInvariantString()); + + redmineManager.ApiClient.Delete(uri, requestOptions); + } + + /// + /// Updates a specified wiki page for a project in Redmine. + /// + /// The instance of the RedmineManager used to process the request. + /// The unique identifier of the project containing the wiki page. + /// The name of the wiki page to be updated. + /// The WikiPage object containing the updated data for the page. + /// Optional parameters for customizing the API request. + public static void UpdateWikiPage(this RedmineManager redmineManager, string projectId, string pageName, + WikiPage wikiPage, RequestOptions requestOptions = null) + { + var payload = redmineManager.Serializer.Serialize(wikiPage); + + if (string.IsNullOrEmpty(payload)) + { + return; + } + + var uri = redmineManager.RedmineApiUrls.ProjectWikiPageUpdate(projectId, pageName); + + redmineManager.ApiClient.Patch(uri, payload, requestOptions); + } + + /// + /// Creates a new wiki page within a specified project in Redmine. + /// + /// The instance of the RedmineManager used to manage API requests. + /// The unique identifier of the project where the wiki page will be created. + /// The name of the new wiki page. + /// The WikiPage object containing the content and metadata for the new page. + /// Additional request options to include in the API call. + /// The created WikiPage object containing the details of the new wiki page. + /// Thrown when the request payload is empty or if the API request fails. + public static WikiPage CreateWikiPage(this RedmineManager redmineManager, string projectId, string pageName, + WikiPage wikiPage, RequestOptions requestOptions = null) + { + var payload = redmineManager.Serializer.Serialize(wikiPage); + + if (string.IsNullOrEmpty(payload)) + { + throw new RedmineException("The payload is empty"); + } + + var uri = redmineManager.RedmineApiUrls.ProjectWikiPageUpdate(projectId, pageName); + + var response = redmineManager.ApiClient.Update(uri, payload, requestOptions); + + return response.DeserializeTo(redmineManager.Serializer); + } + + /// + /// Retrieves a wiki page from a Redmine project using the specified project identifier and page name. + /// + /// The instance of the RedmineManager responsible for managing API requests. + /// The unique identifier of the project containing the wiki page. + /// The name of the wiki page to retrieve. + /// Additional options to include in the API request, such as headers or query parameters. + /// The specific version of the wiki page to retrieve. If 0, the latest version is retrieved. + /// A WikiPage object containing the details of the requested wiki page. + public static WikiPage GetWikiPage(this RedmineManager redmineManager, string projectId, string pageName, + RequestOptions requestOptions = null, uint version = 0) + { + var uri = version == 0 + ? redmineManager.RedmineApiUrls.ProjectWikiPage(projectId, pageName) + : redmineManager.RedmineApiUrls.ProjectWikiPageVersion(projectId, pageName, version.ToInvariantString()); + + var response = redmineManager.ApiClient.Get(uri, requestOptions); + + return response.DeserializeTo(redmineManager.Serializer); + } + + /// + /// Retrieves all wiki pages associated with the specified project. + /// + /// The instance of the RedmineManager used to manage API requests. + /// The unique identifier of the project whose wiki pages are to be fetched. + /// Additional request options to include in the API call. + /// A list of wiki pages associated with the specified project. + public static List GetAllWikiPages(this RedmineManager redmineManager, string projectId, + RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.ProjectWikiIndex(projectId); + + var response = redmineManager.GetInternal(uri, requestOptions); + + return response; + } + + /// + /// Deletes a wiki page, its attachments and its history. If the deleted page is a parent page, its child pages are not + /// deleted but changed as root pages. + /// + /// The instance of the RedmineManager used to manage API requests. + /// The project id or identifier. + /// The wiki page name. + /// Additional request options to include in the API call. + public static void DeleteWikiPage(this RedmineManager redmineManager, string projectId, string pageName, RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.ProjectWikiPageDelete(projectId, pageName); + + redmineManager.ApiClient.Delete(uri, requestOptions); + } + + /// + /// Updates the attachment. + /// + /// + /// The issue identifier. + /// The attachment. + /// Additional request options to include in the API call. + public static void UpdateIssueAttachment(this RedmineManager redmineManager, int issueId, Attachment attachment, RequestOptions requestOptions = null) + { + var attachments = new Attachments + { + {attachment.Id, attachment} + }; + + var data = redmineManager.Serializer.Serialize(attachments); + + var uri = redmineManager.RedmineApiUrls.AttachmentUpdate(issueId.ToInvariantString()); + + redmineManager.ApiClient.Patch(uri, data, requestOptions); + } + + /// + /// + /// + /// + /// query strings. enable to specify multiple values separated by a space " ". + /// number of results in response. + /// skip this number of results in response + /// Optional filters. + /// + /// + /// Returns the search results by the specified condition parameters. + /// + public static PagedResults Search(this RedmineManager redmineManager, string q, int limit = RedmineConstants.DEFAULT_PAGE_SIZE_VALUE, int offset = 0, SearchFilterBuilder searchFilter = null, string impersonateUserName = null) + { + var parameters = CreateSearchParameters(q, limit, offset, searchFilter); + + var response = redmineManager.GetPaginated(new RequestOptions + { + QueryString = parameters, + ImpersonateUser = impersonateUserName + }); + + return response; + } + + private static NameValueCollection CreateSearchParameters(string q, int limit, int offset, SearchFilterBuilder searchFilter) + { + if (q.IsNullOrWhiteSpace()) + { + throw new ArgumentNullException(nameof(q)); + } + + var parameters = new NameValueCollection + { + {RedmineKeys.Q, q}, + {RedmineKeys.LIMIT, limit.ToInvariantString()}, + {RedmineKeys.OFFSET, offset.ToInvariantString()}, + }; + + return searchFilter != null ? searchFilter.Build(parameters) : parameters; + } + + #if !(NET20) + + /// + /// Archives the project asynchronously + /// + /// + /// + /// Additional request options to include in the API call. + /// + public static async Task ArchiveProjectAsync(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.ProjectArchive(projectIdentifier); + + await redmineManager.ApiClient.DeleteAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); + } + + /// + /// Unarchives the project asynchronously + /// + /// + /// + /// Additional request options to include in the API call. + /// + public static async Task UnarchiveProjectAsync(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.ProjectUnarchive(projectIdentifier); + + await redmineManager.ApiClient.DeleteAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); + } + + /// + /// Closes the project asynchronously + /// + /// + /// + /// Additional request options to include in the API call. + /// + public static async Task CloseProjectAsync(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.ProjectClose(projectIdentifier); + + await redmineManager.ApiClient.UpdateAsync(uri, string.Empty, requestOptions, cancellationToken).ConfigureAwait(false); + } + + /// + /// Reopens the project asynchronously + /// + /// + /// + /// Additional request options to include in the API call. + /// + public static async Task ReopenProjectAsync(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.ProjectReopen(projectIdentifier); + + await redmineManager.ApiClient.UpdateAsync(uri, string.Empty, requestOptions, cancellationToken).ConfigureAwait(false); + } + + /// + /// + /// + /// + /// + /// + /// + /// Additional request options to include in the API call. + /// + public static async Task ProjectRepositoryAddRelatedIssueAsync(this RedmineManager redmineManager, string projectIdentifier, string repositoryIdentifier, string revision, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.ProjectRepositoryAddRelatedIssue(projectIdentifier, repositoryIdentifier, revision); + + await redmineManager.ApiClient.CreateAsync(uri, string.Empty ,requestOptions, cancellationToken).ConfigureAwait(false); + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// Additional request options to include in the API call. + /// + public static async Task ProjectRepositoryRemoveRelatedIssueAsync(this RedmineManager redmineManager, string projectIdentifier, string repositoryIdentifier, string revision, string issueIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.ProjectRepositoryRemoveRelatedIssue(projectIdentifier, repositoryIdentifier, revision, issueIdentifier); + + await redmineManager.ApiClient.DeleteAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); + } + + /// + /// + /// + /// + /// + /// Additional request options to include in the API call. + /// + /// + public static async Task> GetProjectNewsAsync(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.ProjectNews(projectIdentifier); + + var response = await redmineManager.ApiClient.GetPagedAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeToPagedResults(redmineManager.Serializer); + } + + /// + /// + /// + /// + /// + /// + /// Additional request options to include in the API call. + /// + /// + /// + public static async Task AddProjectNewsAsync(this RedmineManager redmineManager, string projectIdentifier, News news, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + if (news == null) + { + throw new RedmineException("Argument news is null"); + } + + if (news.Title.IsNullOrWhiteSpace()) + { + throw new RedmineException("News title cannot be blank"); + } + + var payload = redmineManager.Serializer.Serialize(news); + + var uri = redmineManager.RedmineApiUrls.ProjectNews(projectIdentifier); + + var response = await redmineManager.ApiClient.CreateAsync(uri, payload, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeTo(redmineManager.Serializer); + } + + /// + /// + /// + /// + /// + /// Additional request options to include in the API call. + /// + /// + /// + public static async Task> GetProjectMembershipsAsync(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.ProjectMemberships(projectIdentifier); + + var response = await redmineManager.ApiClient.GetPagedAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeToPagedResults(redmineManager.Serializer); + } + + /// + /// + /// + /// + /// + /// Additional request options to include in the API call. + /// + /// + /// + public static async Task> GetProjectFilesAsync(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.ProjectFilesFragment(projectIdentifier); + + var response = await redmineManager.ApiClient.GetPagedAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeToPagedResults(redmineManager.Serializer); + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static async Task> SearchAsync(this RedmineManager redmineManager, + string q, + int limit = RedmineConstants.DEFAULT_PAGE_SIZE_VALUE, + int offset = 0, + SearchFilterBuilder searchFilter = null, + CancellationToken cancellationToken = default) + { + var parameters = CreateSearchParameters(q, limit, offset, searchFilter); + + var response = await redmineManager.GetPagedAsync(new RequestOptions() + { + QueryString = parameters + }, cancellationToken).ConfigureAwait(false); + + return response; + } + + /// + /// + /// + /// + /// Additional request options to include in the API call. + /// + /// + public static async Task GetCurrentUserAsync(this RedmineManager redmineManager, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.CurrentUser(); + + var response = await redmineManager.ApiClient.GetAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeTo(redmineManager.Serializer); + } + + /// + /// Retrieves the account details of the currently authenticated user. + /// + /// The instance of the RedmineManager used to perform the API call. + /// Optional configuration for the API request. + /// + /// Returns the account details of the authenticated user as a MyAccount object. + public static async Task GetMyAccountAsync(this RedmineManager redmineManager, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.MyAccount(); + + var response = await redmineManager.ApiClient.GetAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeTo(redmineManager.Serializer); + } + + /// + /// Creates or updates wiki page asynchronous. + /// + /// The redmine manager. + /// The project identifier. + /// Name of the page. + /// The wiki page. + /// Additional request options to include in the API call. + /// + /// + public static async Task CreateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var payload = redmineManager.Serializer.Serialize(wikiPage); + + if (pageName.IsNullOrWhiteSpace()) + { + throw new RedmineException("Page name cannot be blank"); + } + + if (string.IsNullOrEmpty(payload)) + { + throw new RedmineException("The payload is empty"); + } + + var path = redmineManager.RedmineApiUrls.ProjectWikiPageUpdate(projectId, pageName); + + var response = await redmineManager.ApiClient.UpdateAsync(path, payload, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeTo(redmineManager.Serializer); + } + + /// + /// Creates or updates wiki page asynchronous. + /// + /// The redmine manager. + /// The project identifier. + /// Name of the page. + /// The wiki page. + /// Additional request options to include in the API call. + /// + /// + public static async Task UpdateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var payload = redmineManager.Serializer.Serialize(wikiPage); + + if (string.IsNullOrEmpty(payload)) + { + return; + } + + var url = redmineManager.RedmineApiUrls.ProjectWikiPageUpdate(projectId, pageName); + + await redmineManager.ApiClient.PatchAsync(url, payload, requestOptions, cancellationToken).ConfigureAwait(false); + } + + /// + /// Deletes the wiki page asynchronous. + /// + /// The redmine manager. + /// The project identifier. + /// Name of the page. + /// Additional request options to include in the API call. + /// + /// + public static async Task DeleteWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.ProjectWikiPageDelete(projectId, pageName); + + await redmineManager.ApiClient.DeleteAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); + } + + /// + /// Gets the wiki page asynchronous. + /// + /// The redmine manager. + /// The project identifier. + /// Name of the page. + /// Additional request options to include in the API call. + /// The version. + /// + /// + public static async Task GetWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, RequestOptions requestOptions = null, uint version = 0, CancellationToken cancellationToken = default) + { + var uri = version == 0 + ? redmineManager.RedmineApiUrls.ProjectWikiPage(projectId, pageName) + : redmineManager.RedmineApiUrls.ProjectWikiPageVersion(projectId, pageName, version.ToInvariantString()); + + var response = await redmineManager.ApiClient.GetAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeTo(redmineManager.Serializer); + } + + /// + /// Gets all wiki pages asynchronous. + /// + /// The redmine manager. + /// The project identifier. + /// Additional request options to include in the API call. + /// + /// + public static async Task> GetAllWikiPagesAsync(this RedmineManager redmineManager, string projectId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.ProjectWikiIndex(projectId); + + var response = await redmineManager.ApiClient.GetPagedAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); + + var pages = response.DeserializeToPagedResults(redmineManager.Serializer); + return pages.Items as List; + } + + /// + /// Adds an existing user to a group. This method does not block the calling thread. + /// + /// The redmine manager. + /// The group id. + /// The user id. + /// Additional request options to include in the API call. + /// + /// + /// Returns the Guid associated with the async request. + /// + public static async Task AddUserToGroupAsync(this RedmineManager redmineManager, int groupId, int userId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.GroupUserAdd(groupId.ToInvariantString()); + + var payload = SerializationHelper.SerializeUserId(userId, redmineManager.Serializer); + + await redmineManager.ApiClient.CreateAsync(uri, payload, requestOptions, cancellationToken).ConfigureAwait(false); + } + + /// + /// Removes an user from a group. This method does not block the calling thread. + /// + /// The redmine manager. + /// The group id. + /// The user id. + /// Additional request options to include in the API call. + /// + /// + public static async Task RemoveUserFromGroupAsync(this RedmineManager redmineManager, int groupId, int userId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.GroupUserRemove(groupId.ToInvariantString(), userId.ToInvariantString()); + + await redmineManager.ApiClient.DeleteAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); + } + + /// + /// Adds the watcher asynchronous. + /// + /// The redmine manager. + /// The issue identifier. + /// The user identifier. + /// Additional request options to include in the API call. + /// + /// + public static async Task AddWatcherToIssueAsync(this RedmineManager redmineManager, int issueId, int userId, RequestOptions requestOptions = null , CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.IssueWatcherAdd(issueId.ToInvariantString()); + + var payload = SerializationHelper.SerializeUserId(userId, redmineManager.Serializer); + + await redmineManager.ApiClient.CreateAsync(uri, payload, requestOptions, cancellationToken).ConfigureAwait(false); + } + + /// + /// Removes the watcher asynchronous. + /// + /// The redmine manager. + /// The issue identifier. + /// The user identifier. + /// Additional request options to include in the API call. + /// + /// + public static async Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManager, int issueId, int userId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.IssueWatcherRemove(issueId.ToInvariantString(), userId.ToInvariantString()); + + await redmineManager.ApiClient.DeleteAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); + } + #endif + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Extensions/SemaphoreSlimExtensions.cs b/src/redmine-net-api/Extensions/SemaphoreSlimExtensions.cs new file mode 100644 index 00000000..016dd51f --- /dev/null +++ b/src/redmine-net-api/Extensions/SemaphoreSlimExtensions.cs @@ -0,0 +1,35 @@ +/* +Copyright 2011 - 2025 Adrian Popescu + +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. +*/ + +#if !(NET20) +using System.Threading; +using System.Threading.Tasks; + +namespace Redmine.Net.Api.Extensions; +#if !(NET45_OR_GREATER || NETCOREAPP) +internal static class SemaphoreSlimExtensions +{ + + public static Task WaitAsync(this SemaphoreSlim semaphore, CancellationToken cancellationToken = default) + { + return Task.Factory.StartNew(() => semaphore.Wait(cancellationToken) + , CancellationToken.None + , TaskCreationOptions.None + , TaskScheduler.Default); + } +} +#endif +#endif diff --git a/src/redmine-net-api/Extensions/StringExtensions.cs b/src/redmine-net-api/Extensions/StringExtensions.cs new file mode 100644 index 00000000..a881f14d --- /dev/null +++ b/src/redmine-net-api/Extensions/StringExtensions.cs @@ -0,0 +1,203 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Security; +using System.Text.RegularExpressions; + +namespace Redmine.Net.Api.Extensions +{ + /// + /// + /// + public static class StringExtensions + { + /// + /// Determines whether a string is null, empty, or consists only of white-space characters. + /// + /// The string to evaluate. + /// True if the string is null, empty, or whitespace; otherwise, false. + public static bool IsNullOrWhiteSpace(this string value) + { + if (value == null) + { + return true; + } + + foreach (var ch in value) + { + if (!char.IsWhiteSpace(ch)) + { + return false; + } + } + + return true; + } + + /// + /// Truncates a string to the specified maximum length if it exceeds that length. + /// + /// The string to truncate. + /// The maximum allowed length for the string. + /// The truncated string if its length exceeds the maximum length; otherwise, the original string. + public static string Truncate(this string text, int maximumLength) + { + if (text.IsNullOrWhiteSpace() || maximumLength < 1 || text.Length <= maximumLength) + { + return text; + } + + #if (NET5_0_OR_GREATER) + return text.AsSpan()[..maximumLength].ToString(); + #else + return text.Substring(0, maximumLength); + #endif + } + + /// + /// Lower case based on invariant culture. + /// + /// + /// + [SuppressMessage("ReSharper", "CA1308")] + public static string ToLowerInv(this string text) + { + return text.IsNullOrWhiteSpace() ? text : text.ToLowerInvariant(); + } + + /// + /// Transforms a string into a SecureString. + /// + /// + /// The string to transform. + /// + /// + /// A secure string representing the contents of the original string. + /// + internal static SecureString ToSecureString(this string value) + { + if (value.IsNullOrWhiteSpace()) + { + return null; + } + + var rv = new SecureString(); + + foreach (var ch in value) + { + rv.AppendChar(ch); + } + + return rv; + } + + /// + /// Removes the trailing slash ('/' or '\') from the end of the string if it exists. + /// + /// The string to process. + /// The input string without a trailing slash, or the original string if no trailing slash exists. + internal static string RemoveTrailingSlash(this string s) + { + if (string.IsNullOrEmpty(s)) + { + return s; + } + + #if (NET5_0_OR_GREATER) + if (s.EndsWith('/') || s.EndsWith('\\')) + { + return s.AsSpan()[..(s.Length - 1)].ToString(); + } + #else + if (s.EndsWith("/", StringComparison.OrdinalIgnoreCase) || s.EndsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + return s.Substring(0, s.Length - 1); + } + #endif + + return s; + } + + /// + /// Returns the specified string value if it is neither null, empty, nor consists only of white-space characters; otherwise, returns the fallback string. + /// + /// The primary string value to evaluate. + /// The fallback string to return if the primary string is null, empty, or consists of only white-space characters. + /// The original string if it is valid; otherwise, the fallback string. + internal static string ValueOrFallback(this string value, string fallback) + { + return !value.IsNullOrWhiteSpace() ? value : fallback; + } + + /// + /// Converts a value of a struct type to its invariant culture string representation. + /// + /// The struct type of the value. + /// The value to convert to a string. + /// The invariant culture string representation of the value. + internal static string ToInvariantString(this T value) where T : struct + { + return value switch + { + sbyte v => v.ToString(CultureInfo.InvariantCulture), + byte v => v.ToString(CultureInfo.InvariantCulture), + short v => v.ToString(CultureInfo.InvariantCulture), + ushort v => v.ToString(CultureInfo.InvariantCulture), + int v => v.ToString(CultureInfo.InvariantCulture), + uint v => v.ToString(CultureInfo.InvariantCulture), + long v => v.ToString(CultureInfo.InvariantCulture), + ulong v => v.ToString(CultureInfo.InvariantCulture), + float v => v.ToString("G7", CultureInfo.InvariantCulture), // Specify precision explicitly for backward compatibility + double v => v.ToString("G15", CultureInfo.InvariantCulture), // Specify precision explicitly for backward compatibility + decimal v => v.ToString(CultureInfo.InvariantCulture), + TimeSpan ts => ts.ToString(), + DateTime d => d.ToString(CultureInfo.InvariantCulture), + #pragma warning disable CA1308 + bool b => b ? "true" : "false", + #pragma warning restore CA1308 + _ => value.ToString(), + }; + } + + private const string CR = "\r"; + private const string LR = "\n"; + private const string CRLR = $"{CR}{LR}"; + + /// + /// Replaces all line endings in the input string with the specified replacement string. + /// + /// The string in which line endings will be replaced. + /// The string to replace line endings with. Defaults to a combination of carriage return and line feed. + /// The input string with all line endings replaced by the specified replacement string. + internal static string ReplaceEndings(this string input, string replacement = CRLR) + { + if (input.IsNullOrWhiteSpace()) + { + return input; + } + + #if NET6_0_OR_GREATER + input = input.ReplaceLineEndings(replacement); + #else + input = Regex.Replace(input, $"{CRLR}|{CR}|{LR}", replacement); + #endif + return input; + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Extensions/TaskExtensions.cs b/src/redmine-net-api/Extensions/TaskExtensions.cs new file mode 100644 index 00000000..dd182846 --- /dev/null +++ b/src/redmine-net-api/Extensions/TaskExtensions.cs @@ -0,0 +1,60 @@ +/* +Copyright 2011 - 2025 Adrian Popescu + +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. +*/ + +#if !(NET20) +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Redmine.Net.Api.Extensions; + +internal static class TaskExtensions +{ + public static T GetAwaiterResult(this Task task) + { + return task.GetAwaiter().GetResult(); + } + + public static TResult Synchronize(Func> function) + { + return Task.Factory.StartNew(function, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default) + .Unwrap().GetAwaiter().GetResult(); + } + + public static void Synchronize(Func function) + { + Task.Factory.StartNew(function, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default) + .Unwrap().GetAwaiter().GetResult(); + } + + #if !(NET45_OR_GREATER || NETCOREAPP) + public static Task WhenAll(IEnumerable> tasks) + { + var clone = tasks.ToArray(); + + var x = Task.Factory.StartNew(() => + { + Task.WaitAll(clone); + return clone.Select(t => t.Result).ToArray(); + }, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); + + return default; + } + #endif +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Features/CallerArgumentExpressionAttribute.cs b/src/redmine-net-api/Features/CallerArgumentExpressionAttribute.cs new file mode 100644 index 00000000..62165823 --- /dev/null +++ b/src/redmine-net-api/Features/CallerArgumentExpressionAttribute.cs @@ -0,0 +1,31 @@ +#if NET20_OR_GREATER +#pragma warning disable +#nullable enable annotations + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices +{ + /// + /// An attribute that allows parameters to receive the expression of other parameters. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] + internal sealed class CallerArgumentExpressionAttribute : global::System.Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The condition parameter value. + public CallerArgumentExpressionAttribute(string parameterName) + { + ParameterName = parameterName; + } + + /// + /// Gets the parameter name the expression is retrieved from. + /// + public string ParameterName { get; } + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Features/IsExternalInit.cs b/src/redmine-net-api/Features/IsExternalInit.cs new file mode 100644 index 00000000..0de2af11 --- /dev/null +++ b/src/redmine-net-api/Features/IsExternalInit.cs @@ -0,0 +1,21 @@ +#if NET20_OR_GREATER + +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices +{ + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal static class IsExternalInit + { + } +} + +#endif + + + + + diff --git a/src/redmine-net-api/Features/NotNullAttribute.cs b/src/redmine-net-api/Features/NotNullAttribute.cs new file mode 100644 index 00000000..8aa30659 --- /dev/null +++ b/src/redmine-net-api/Features/NotNullAttribute.cs @@ -0,0 +1,25 @@ +#if NET20_OR_GREATER +// +#pragma warning disable +#nullable enable annotations + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// ReSharper disable once CheckNamespace +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// Specifies that an output will not be null even if the corresponding type allows it. + /// Specifies that an input argument was not null when the call returns. + /// + [global::System.AttributeUsage( + global::System.AttributeTargets.Field | + global::System.AttributeTargets.Parameter | + global::System.AttributeTargets.Property | + global::System.AttributeTargets.ReturnValue, + Inherited = false)] + internal sealed class NotNullAttribute : global::System.Attribute + { + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Http/Clients/HttpClient/HttpClientProvider.cs b/src/redmine-net-api/Http/Clients/HttpClient/HttpClientProvider.cs new file mode 100644 index 00000000..3932402f --- /dev/null +++ b/src/redmine-net-api/Http/Clients/HttpClient/HttpClientProvider.cs @@ -0,0 +1,257 @@ +#if !NET20 +using System; +using System.Net.Http; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using Redmine.Net.Api.Common; +using Redmine.Net.Api.Options; + +namespace Redmine.Net.Api.Http.Clients.HttpClient; + +internal static class HttpClientProvider +{ + private static System.Net.Http.HttpClient _client; + + /// + /// Gets an HttpClient instance. If an existing client is provided, it is returned; otherwise, a new one is created. + /// + public static System.Net.Http.HttpClient GetOrCreateHttpClient(System.Net.Http.HttpClient httpClient, + RedmineManagerOptions options) + { + if (_client != null) + { + return _client; + } + + _client = httpClient ?? CreateClient(options); + + return _client; + } + + /// + /// Creates a new HttpClient instance configured with the specified options. + /// + private static System.Net.Http.HttpClient CreateClient(RedmineManagerOptions redmineManagerOptions) + { + ArgumentVerifier.ThrowIfNull(redmineManagerOptions, nameof(redmineManagerOptions)); + + var handler = + #if NET + CreateSocketHandler(redmineManagerOptions); + #elif NETFRAMEWORK + CreateHandler(redmineManagerOptions); + #endif + + var client = new System.Net.Http.HttpClient(handler, disposeHandler: true); + + if (redmineManagerOptions.BaseAddress != null) + { + client.BaseAddress = redmineManagerOptions.BaseAddress; + } + + if (redmineManagerOptions.ApiClientOptions is not RedmineHttpClientOptions options) + { + return client; + } + + if (options.Timeout.HasValue) + { + client.Timeout = options.Timeout.Value; + } + + if (options.MaxResponseContentBufferSize.HasValue) + { + client.MaxResponseContentBufferSize = options.MaxResponseContentBufferSize.Value; + } + +#if NET5_0_OR_GREATER + if (options.DefaultRequestVersion != null) + { + client.DefaultRequestVersion = options.DefaultRequestVersion; + } + + if (options.DefaultVersionPolicy != null) + { + client.DefaultVersionPolicy = options.DefaultVersionPolicy.Value; + } +#endif + + return client; + } + +#if NET + private static SocketsHttpHandler CreateSocketHandler(RedmineManagerOptions redmineManagerOptions) + { + var handler = new SocketsHttpHandler() + { + // Limit the lifetime of connections to better respect any DNS changes + PooledConnectionLifetime = TimeSpan.FromMinutes(2), + + // Check cert revocation + SslOptions = new SslClientAuthenticationOptions() + { + CertificateRevocationCheckMode = X509RevocationMode.Online, + }, + }; + + if (redmineManagerOptions.ApiClientOptions is not RedmineHttpClientOptions options) + { + return handler; + } + + if (options.CookieContainer != null) + { + handler.CookieContainer = options.CookieContainer; + } + + handler.Credentials = options.Credentials; + handler.Proxy = options.Proxy; + + if (options.AutoRedirect.HasValue) + { + handler.AllowAutoRedirect = options.AutoRedirect.Value; + } + + if (options.DecompressionFormat.HasValue) + { + handler.AutomaticDecompression = options.DecompressionFormat.Value; + } + + if (options.PreAuthenticate.HasValue) + { + handler.PreAuthenticate = options.PreAuthenticate.Value; + } + + if (options.UseCookies.HasValue) + { + handler.UseCookies = options.UseCookies.Value; + } + + if (options.UseProxy.HasValue) + { + handler.UseProxy = options.UseProxy.Value; + } + + if (options.MaxAutomaticRedirections.HasValue) + { + handler.MaxAutomaticRedirections = options.MaxAutomaticRedirections.Value; + } + + handler.DefaultProxyCredentials = options.DefaultProxyCredentials; + + if (options.MaxConnectionsPerServer.HasValue) + { + handler.MaxConnectionsPerServer = options.MaxConnectionsPerServer.Value; + } + + if (options.MaxResponseHeadersLength.HasValue) + { + handler.MaxResponseHeadersLength = options.MaxResponseHeadersLength.Value; + } + +#if NET8_0_OR_GREATER + handler.MeterFactory = options.MeterFactory; +#endif + + return handler; + } +#elif NETFRAMEWORK + private static HttpClientHandler CreateHandler(RedmineManagerOptions redmineManagerOptions) + { + var handler = new HttpClientHandler(); + return ConfigureHandler(handler, redmineManagerOptions); + } + + private static HttpClientHandler ConfigureHandler(HttpClientHandler handler, RedmineManagerOptions redmineManagerOptions) + { + if (redmineManagerOptions.ApiClientOptions is not RedmineHttpClientOptions options) + { + return handler; + } + + if (options.UseDefaultCredentials.HasValue) + { + handler.UseDefaultCredentials = options.UseDefaultCredentials.Value; + } + + if (options.CookieContainer != null) + { + handler.CookieContainer = options.CookieContainer; + } + + if (handler.SupportsAutomaticDecompression && options.DecompressionFormat.HasValue) + { + handler.AutomaticDecompression = options.DecompressionFormat.Value; + } + + if (handler.SupportsRedirectConfiguration) + { + if (options.AutoRedirect.HasValue) + { + handler.AllowAutoRedirect = options.AutoRedirect.Value; + } + + if (options.MaxAutomaticRedirections.HasValue) + { + handler.MaxAutomaticRedirections = options.MaxAutomaticRedirections.Value; + } + } + + if (options.ClientCertificateOptions != default) + { + handler.ClientCertificateOptions = options.ClientCertificateOptions; + } + + handler.Credentials = options.Credentials; + + if (options.UseProxy != null) + { + handler.UseProxy = options.UseProxy.Value; + if (handler.UseProxy && options.Proxy != null) + { + handler.Proxy = options.Proxy; + } + } + + if (options.PreAuthenticate.HasValue) + { + handler.PreAuthenticate = options.PreAuthenticate.Value; + } + + if (options.UseCookies.HasValue) + { + handler.UseCookies = options.UseCookies.Value; + } + + if (options.MaxRequestContentBufferSize.HasValue) + { + handler.MaxRequestContentBufferSize = options.MaxRequestContentBufferSize.Value; + } + +#if NET471_OR_GREATER + handler.CheckCertificateRevocationList = options.CheckCertificateRevocationList; + + if (options.DefaultProxyCredentials != null) + handler.DefaultProxyCredentials = options.DefaultProxyCredentials; + + if (options.ServerCertificateCustomValidationCallback != null) + handler.ServerCertificateCustomValidationCallback = options.ServerCertificateCustomValidationCallback; + + if (options.ServerCertificateValidationCallback != null) + handler.ServerCertificateCustomValidationCallback = options.ServerCertificateValidationCallback; + + handler.SslProtocols = options.SslProtocols; + + if (options.MaxConnectionsPerServer.HasValue) + handler.MaxConnectionsPerServer = options.MaxConnectionsPerServer.Value; + + if (options.MaxResponseHeadersLength.HasValue) + handler.MaxResponseHeadersLength = options.MaxResponseHeadersLength.Value; +#endif + + return handler; + } +#endif +} + +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Http/Clients/HttpClient/HttpContentExtensions.cs b/src/redmine-net-api/Http/Clients/HttpClient/HttpContentExtensions.cs new file mode 100644 index 00000000..c1b2515c --- /dev/null +++ b/src/redmine-net-api/Http/Clients/HttpClient/HttpContentExtensions.cs @@ -0,0 +1,20 @@ +#if !NET20 + +using System.Net; + +namespace Redmine.Net.Api.Http.Clients.HttpClient; + +internal static class HttpContentExtensions +{ + public static bool IsUnprocessableEntity(this HttpStatusCode statusCode) + { + return +#if NET5_0_OR_GREATER + statusCode == HttpStatusCode.UnprocessableEntity; +#else + (int)statusCode == 422; +#endif + } +} + +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Http/Clients/HttpClient/HttpContentPolyfills.cs b/src/redmine-net-api/Http/Clients/HttpClient/HttpContentPolyfills.cs new file mode 100644 index 00000000..ec5d7f4d --- /dev/null +++ b/src/redmine-net-api/Http/Clients/HttpClient/HttpContentPolyfills.cs @@ -0,0 +1,35 @@ + +#if !(NET20 || NET5_0_OR_GREATER) + +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Redmine.Net.Api.Http.Clients.HttpClient; + +internal static class HttpContentPolyfills +{ + internal static Task ReadAsStringAsync(this HttpContent httpContent, CancellationToken cancellationToken) + => httpContent.ReadAsStringAsync( +#if !NETFRAMEWORK + cancellationToken +#endif + ); + + internal static Task ReadAsStreamAsync(this HttpContent httpContent, CancellationToken cancellationToken) + => httpContent.ReadAsStreamAsync( +#if !NETFRAMEWORK + cancellationToken +#endif + ); + + internal static Task ReadAsByteArrayAsync(this HttpContent httpContent, CancellationToken cancellationToken) + => httpContent.ReadAsByteArrayAsync( +#if !NETFRAMEWORK + cancellationToken +#endif + ); +} + +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Http/Clients/HttpClient/HttpResponseHeadersExtensions.cs b/src/redmine-net-api/Http/Clients/HttpClient/HttpResponseHeadersExtensions.cs new file mode 100644 index 00000000..37b44dcb --- /dev/null +++ b/src/redmine-net-api/Http/Clients/HttpClient/HttpResponseHeadersExtensions.cs @@ -0,0 +1,22 @@ +#if !NET20 +using System.Collections.Specialized; +using System.Net.Http.Headers; + +namespace Redmine.Net.Api.Http.Clients.HttpClient; + +internal static class HttpResponseHeadersExtensions +{ + public static NameValueCollection ToNameValueCollection(this HttpResponseHeaders headers) + { + if (headers == null) return null; + + var collection = new NameValueCollection(); + foreach (var header in headers) + { + var combinedValue = string.Join(", ", header.Value); + collection.Add(header.Key, combinedValue); + } + return collection; + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Http/Clients/HttpClient/IRedmineHttpClientOptions.cs b/src/redmine-net-api/Http/Clients/HttpClient/IRedmineHttpClientOptions.cs new file mode 100644 index 00000000..d8f6d9d2 --- /dev/null +++ b/src/redmine-net-api/Http/Clients/HttpClient/IRedmineHttpClientOptions.cs @@ -0,0 +1,88 @@ +#if NET40_OR_GREATER || NET +using System; +using System.Net; +using System.Net.Http; +using System.Net.Security; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +#if NET8_0_OR_GREATER +using System.Diagnostics.Metrics; +#endif + + +namespace Redmine.Net.Api.Http.Clients.HttpClient; + +/// +/// +/// +public interface IRedmineHttpClientOptions : IRedmineApiClientOptions +{ + /// + /// + /// + ClientCertificateOption ClientCertificateOptions { get; set; } + +#if NET471_OR_GREATER || NET + /// + /// + /// + ICredentials DefaultProxyCredentials { get; set; } + + /// + /// + /// + Func ServerCertificateCustomValidationCallback { get; set; } + + /// + /// + /// + SslProtocols SslProtocols { get; set; } +#endif + + /// + /// + /// + public +#if NET || NET471_OR_GREATER + Func +#else + RemoteCertificateValidationCallback +#endif + ServerCertificateValidationCallback { get; set; } + +#if NET8_0_OR_GREATER + /// + /// + /// + public IMeterFactory MeterFactory { get; set; } +#endif + + /// + /// + /// + bool SupportsAutomaticDecompression { get; set; } + + /// + /// + /// + bool SupportsProxy { get; set; } + + /// + /// + /// + bool SupportsRedirectConfiguration { get; set; } + + /// + /// + /// + Version DefaultRequestVersion { get; set; } + +#if NET + /// + /// + /// + HttpVersionPolicy? DefaultVersionPolicy { get; set; } +#endif +} + +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.Async.cs b/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.Async.cs new file mode 100644 index 00000000..6f2f7e10 --- /dev/null +++ b/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.Async.cs @@ -0,0 +1,155 @@ +#if !NET20 +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http.Constants; +using Redmine.Net.Api.Http.Helpers; +using Redmine.Net.Api.Http.Messages; + +namespace Redmine.Net.Api.Http.Clients.HttpClient; + +internal sealed partial class InternalRedmineApiHttpClient +{ + protected override async Task HandleRequestAsync(string address, string verb, RequestOptions requestOptions = null, + object content = null, IProgress progress = null, CancellationToken cancellationToken = default) + { + var httpMethod = GetHttpMethod(verb); + using var requestMessage = CreateRequestMessage(address, httpMethod, requestOptions, content as HttpContent); + var response = await SendAsync(requestMessage, progress: progress, cancellationToken: cancellationToken).ConfigureAwait(false); + return response; + } + + private async Task SendAsync(HttpRequestMessage requestMessage, IProgress progress = null, CancellationToken cancellationToken = default) + { + try + { + using (var httpResponseMessage = await _httpClient + .SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken) + .ConfigureAwait(false)) + { + if (httpResponseMessage.IsSuccessStatusCode) + { + if (httpResponseMessage.StatusCode == HttpStatusCode.NoContent) + { + return CreateApiResponseMessage(httpResponseMessage.Headers, HttpStatusCode.NoContent, []); + } + + byte[] data; + + if (requestMessage.Method == HttpMethod.Get && progress != null) + { + data = await DownloadWithProgressAsync(httpResponseMessage.Content, progress, cancellationToken) + .ConfigureAwait(false); + } + else + { + data = await httpResponseMessage.Content.ReadAsByteArrayAsync(cancellationToken) + .ConfigureAwait(false); + } + + return CreateApiResponseMessage(httpResponseMessage.Headers, httpResponseMessage.StatusCode, data); + } + + var statusCode = (int)httpResponseMessage.StatusCode; + using (var stream = await httpResponseMessage.Content.ReadAsStreamAsync(cancellationToken) + .ConfigureAwait(false)) + { + var url = requestMessage.RequestUri?.ToString(); + var message = httpResponseMessage.ReasonPhrase; + + throw statusCode switch + { + HttpConstants.StatusCodes.NotFound => new RedmineNotFoundException(message, url), + HttpConstants.StatusCodes.Unauthorized => new RedmineUnauthorizedException(message, url), + HttpConstants.StatusCodes.Forbidden => new RedmineForbiddenException(message, url), + HttpConstants.StatusCodes.UnprocessableEntity => RedmineExceptionHelper.CreateUnprocessableEntityException(url, stream, null, Serializer), + HttpConstants.StatusCodes.NotAcceptable => new RedmineNotAcceptableException(message), + _ => new RedmineApiException(message, url, statusCode), + }; + } + } + } + catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested) + { + throw new RedmineOperationCanceledException(ex.Message, requestMessage.RequestUri, ex); + } + catch (OperationCanceledException ex) when (ex.InnerException is TimeoutException tex) + { + throw new RedmineTimeoutException(tex.Message, requestMessage.RequestUri, tex); + } + catch (TaskCanceledException tcex) when (cancellationToken.IsCancellationRequested) + { + throw new RedmineOperationCanceledException(tcex.Message, requestMessage.RequestUri, tcex); + } + catch (TaskCanceledException tce) + { + throw new RedmineTimeoutException(tce.Message, requestMessage.RequestUri, tce); + } + catch (HttpRequestException ex) + { + throw new RedmineApiException(ex.Message, requestMessage.RequestUri, HttpConstants.StatusCodes.Unknown, ex); + } + catch (Exception ex) when (ex is not RedmineException) + { + throw new RedmineApiException(ex.Message, requestMessage.RequestUri, HttpConstants.StatusCodes.Unknown, ex); + } + } + + private static async Task DownloadWithProgressAsync(HttpContent httpContent, IProgress progress = null, CancellationToken cancellationToken = default) + { + var contentLength = httpContent.Headers.ContentLength ?? -1; + byte[] data; + + if (contentLength > 0) + { + using (var stream = await httpContent.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false)) + { + data = new byte[contentLength]; + int bytesRead; + var totalBytesRead = 0; + var buffer = new byte[8192]; + + while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) > 0) + { + cancellationToken.ThrowIfCancellationRequested(); + + Buffer.BlockCopy(buffer, 0, data, totalBytesRead, bytesRead); + totalBytesRead += bytesRead; + + var progressPercentage = (int)(totalBytesRead * 100 / contentLength); + progress?.Report(progressPercentage); + ClientHelper.ReportProgress(progress, contentLength, totalBytesRead); + } + } + } + else + { + data = await httpContent.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); + progress?.Report(100); + } + + return data; + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.cs b/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.cs new file mode 100644 index 00000000..c785145c --- /dev/null +++ b/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.cs @@ -0,0 +1,188 @@ +#if !NET20 +/* + Copyright 2011 - 2024 Adrian Popescu + + 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. +*/ + +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using Redmine.Net.Api.Authentication; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http.Constants; +using Redmine.Net.Api.Http.Messages; +using Redmine.Net.Api.Options; + +namespace Redmine.Net.Api.Http.Clients.HttpClient; + +internal sealed partial class InternalRedmineApiHttpClient : RedmineApiClient +{ + private static readonly HttpMethod PatchMethod = new HttpMethod("PATCH"); + private static readonly Encoding DefaultEncoding = Encoding.UTF8; + + private readonly System.Net.Http.HttpClient _httpClient; + + public InternalRedmineApiHttpClient(RedmineManagerOptions redmineManagerOptions) + : this(null, redmineManagerOptions) + { + _httpClient = HttpClientProvider.GetOrCreateHttpClient(null, redmineManagerOptions); + } + + public InternalRedmineApiHttpClient(System.Net.Http.HttpClient httpClient, + RedmineManagerOptions redmineManagerOptions) + : base(redmineManagerOptions) + { + _httpClient = httpClient; + } + + protected override object CreateContentFromPayload(string payload) + { + return new StringContent(payload, DefaultEncoding, Serializer.ContentType); + } + + protected override object CreateContentFromBytes(byte[] data) + { + var content = new ByteArrayContent(data); + content.Headers.ContentType = new MediaTypeHeaderValue(RedmineConstants.CONTENT_TYPE_APPLICATION_STREAM); + return content; + } + + protected override RedmineApiResponse HandleRequest(string address, string verb, + RequestOptions requestOptions = null, + object content = null, IProgress progress = null) + { + var httpMethod = GetHttpMethod(verb); + using (var requestMessage = CreateRequestMessage(address, httpMethod, requestOptions, content as HttpContent)) + { + var response = Send(requestMessage, progress); + return response; + } + } + + private RedmineApiResponse Send(HttpRequestMessage requestMessage, IProgress progress = null) + { + return TaskExtensions.Synchronize(() => SendAsync(requestMessage, progress)); + } + + private HttpRequestMessage CreateRequestMessage(string address, HttpMethod method, + RequestOptions requestOptions = null, HttpContent content = null) + { + var httpRequest = new HttpRequestMessage(method, address); + + switch (Credentials) + { + case RedmineApiKeyAuthentication: + httpRequest.Headers.Add(RedmineConstants.API_KEY_AUTHORIZATION_HEADER_KEY, Credentials.Token); + break; + case RedmineBasicAuthentication: + httpRequest.Headers.Add(RedmineConstants.AUTHORIZATION_HEADER_KEY, Credentials.Token); + break; + } + + if (requestOptions != null) + { + if (requestOptions.QueryString != null) + { + var uriToBeAppended = httpRequest.RequestUri.ToString(); + var queryIndex = uriToBeAppended.IndexOf("?", StringComparison.Ordinal); + var hasQuery = queryIndex != -1; + + var sb = new StringBuilder(); + sb.Append('\\'); + sb.Append(uriToBeAppended); + for (var index = 0; index < requestOptions.QueryString.Count; ++index) + { + var value = requestOptions.QueryString[index]; + + if (value == null) + { + continue; + } + + var key = requestOptions.QueryString.Keys[index]; + + sb.Append(hasQuery ? '&' : '?'); + sb.Append(Uri.EscapeDataString(key)); + sb.Append('='); + sb.Append(Uri.EscapeDataString(value)); + hasQuery = true; + } + + var uriString = sb.ToString(); + + httpRequest.RequestUri = new Uri(uriString, UriKind.RelativeOrAbsolute); + } + + if (!requestOptions.ImpersonateUser.IsNullOrWhiteSpace()) + { + httpRequest.Headers.Add(RedmineConstants.IMPERSONATE_HEADER_KEY, requestOptions.ImpersonateUser); + } + + if (!requestOptions.Accept.IsNullOrWhiteSpace()) + { + httpRequest.Headers.Accept.ParseAdd(requestOptions.Accept); + } + + if (!requestOptions.UserAgent.IsNullOrWhiteSpace()) + { + httpRequest.Headers.UserAgent.ParseAdd(requestOptions.UserAgent); + } + + if (requestOptions.Headers != null) + { + foreach (var header in requestOptions.Headers) + { + httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + } + } + + if (content == null) + { + return httpRequest; + } + + httpRequest.Content = content; + if (requestOptions?.ContentType != null) + { + content.Headers.ContentType = new MediaTypeHeaderValue(requestOptions.ContentType); + } + + return httpRequest; + } + + private static RedmineApiResponse CreateApiResponseMessage(HttpResponseHeaders headers, HttpStatusCode statusCode, byte[] content) => new RedmineApiResponse() + { + Content = content, + Headers = headers.ToNameValueCollection(), + StatusCode = statusCode, + }; + + private static HttpMethod GetHttpMethod(string verb) + { + return verb switch + { + HttpConstants.HttpVerbs.GET => HttpMethod.Get, + HttpConstants.HttpVerbs.POST => HttpMethod.Post, + HttpConstants.HttpVerbs.PUT => HttpMethod.Put, + HttpConstants.HttpVerbs.PATCH => PatchMethod, + HttpConstants.HttpVerbs.DELETE => HttpMethod.Delete, + HttpConstants.HttpVerbs.DOWNLOAD => HttpMethod.Get, + _ => throw new ArgumentException($"Unsupported HTTP verb: {verb}") + }; + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Http/Clients/HttpClient/RedmineHttpClientOptions.cs b/src/redmine-net-api/Http/Clients/HttpClient/RedmineHttpClientOptions.cs new file mode 100644 index 00000000..dc8e0b6a --- /dev/null +++ b/src/redmine-net-api/Http/Clients/HttpClient/RedmineHttpClientOptions.cs @@ -0,0 +1,75 @@ +#if !NET20 +using System; +using System.Net; +using System.Net.Http; +using System.Net.Security; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +#if NET8_0_OR_GREATER +using System.Diagnostics.Metrics; +#endif + +namespace Redmine.Net.Api.Http.Clients.HttpClient; + +/// +/// +/// +public sealed class RedmineHttpClientOptions: RedmineApiClientOptions +{ +#if NET8_0_OR_GREATER + /// + /// + /// + public IMeterFactory MeterFactory { get; set; } +#endif + + /// + /// + /// + public Version DefaultRequestVersion { get; set; } + +#if NET + /// + /// + /// + public HttpVersionPolicy? DefaultVersionPolicy { get; set; } +#endif + /// + /// + /// + public ICredentials DefaultProxyCredentials { get; set; } + + /// + /// + /// + public ClientCertificateOption ClientCertificateOptions { get; set; } + + + +#if NETFRAMEWORK + /// + /// + /// + public Func ServerCertificateCustomValidationCallback + { + get; + set; + } + + /// + /// + /// + public SslProtocols SslProtocols { get; set; } + #endif + /// + /// + /// + public +#if NET || NET471_OR_GREATER + Func +#else + RemoteCertificateValidationCallback +#endif + ServerCertificateValidationCallback { get; set; } +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.Async.cs b/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.Async.cs new file mode 100644 index 00000000..94277181 --- /dev/null +++ b/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.Async.cs @@ -0,0 +1,144 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +#if!(NET20) +using System; +using System.Collections.Specialized; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Http.Messages; + +namespace Redmine.Net.Api.Http.Clients.WebClient +{ + /// + /// + /// + internal sealed partial class InternalRedmineApiWebClient + { + protected override async Task HandleRequestAsync(string address, string verb, RequestOptions requestOptions = null, object content = null, + IProgress progress = null, CancellationToken cancellationToken = default) + { + LogRequest(verb, address, requestOptions); + + var response = await SendAsync(CreateRequestMessage(address, verb, Serializer, requestOptions, content as RedmineApiRequestContent), progress, cancellationToken: cancellationToken).ConfigureAwait(false); + + LogResponse((int)response.StatusCode); + + return response; + } + + private async Task SendAsync(RedmineApiRequest requestMessage, IProgress progress = null, CancellationToken cancellationToken = default) + { + System.Net.WebClient webClient = null; + byte[] response = null; + HttpStatusCode? statusCode = null; + NameValueCollection responseHeaders = null; + CancellationTokenRegistration cancellationTokenRegistration = default; + + try + { + webClient = _webClientFunc(); + cancellationTokenRegistration = + cancellationToken.Register( + static state => ((System.Net.WebClient)state).CancelAsync(), + webClient + ); + + cancellationToken.ThrowIfCancellationRequested(); + + if (progress != null) + { + webClient.DownloadProgressChanged += (_, e) => { progress.Report(e.ProgressPercentage); }; + } + + if (requestMessage.QueryString != null) + { + webClient.QueryString = requestMessage.QueryString; + } + + webClient.ApplyHeaders(requestMessage, Credentials); + + if (IsGetOrDownload(requestMessage.Method)) + { + response = await webClient.DownloadDataTaskAsync(requestMessage.RequestUri) + .ConfigureAwait(false); + } + else + { + byte[] payload; + if (requestMessage.Content != null) + { + webClient.Headers.Add(HttpRequestHeader.ContentType, requestMessage.Content.ContentType); + payload = requestMessage.Content.Body; + } + else + { + payload = EmptyBytes; + } + + response = await webClient + .UploadDataTaskAsync(requestMessage.RequestUri, requestMessage.Method, payload) + .ConfigureAwait(false); + } + + responseHeaders = webClient.ResponseHeaders; + statusCode = webClient.GetStatusCode(); + } + catch (WebException ex) when (ex.Status == WebExceptionStatus.RequestCanceled) + { + throw new RedmineOperationCanceledException(ex.Message, requestMessage.RequestUri, ex); + } + catch (WebException ex) when (ex.Status == WebExceptionStatus.Timeout) + { + throw new RedmineTimeoutException(ex.Message, requestMessage.RequestUri, ex); + } + catch (WebException webException)when (webException.Status == WebExceptionStatus.ProtocolError) + { + HandleResponseException(webException, requestMessage.RequestUri, Serializer); + } + catch (OperationCanceledException ex) + { + throw new RedmineOperationCanceledException(ex.Message, requestMessage.RequestUri, ex); + } + catch (Exception ex) + { + throw new RedmineApiException(ex.Message, requestMessage.RequestUri, null, ex); + } + finally + { + #if NETFRAMEWORK + cancellationTokenRegistration.Dispose(); + #else + await cancellationTokenRegistration.DisposeAsync().ConfigureAwait(false); + #endif + + webClient?.Dispose(); + } + + return new RedmineApiResponse() + { + Headers = responseHeaders, + Content = response, + StatusCode = statusCode ?? HttpStatusCode.OK, + }; + } + } +} + +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.cs b/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.cs new file mode 100644 index 00000000..b4eb7a7b --- /dev/null +++ b/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.cs @@ -0,0 +1,220 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections.Specialized; +using System.Net; +using System.Text; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http.Constants; +using Redmine.Net.Api.Http.Helpers; +using Redmine.Net.Api.Http.Messages; +using Redmine.Net.Api.Options; +using Redmine.Net.Api.Serialization; + +namespace Redmine.Net.Api.Http.Clients.WebClient +{ + /// + /// + /// + internal sealed partial class InternalRedmineApiWebClient : RedmineApiClient + { + private static readonly byte[] EmptyBytes = Encoding.UTF8.GetBytes(string.Empty); + private readonly Func _webClientFunc; + + public InternalRedmineApiWebClient(RedmineManagerOptions redmineManagerOptions) + : this(() => new InternalWebClient(redmineManagerOptions), redmineManagerOptions) + { + } + + public InternalRedmineApiWebClient(Func webClientFunc, RedmineManagerOptions redmineManagerOptions) + : base(redmineManagerOptions) + { + _webClientFunc = webClientFunc; + } + + protected override object CreateContentFromPayload(string payload) + { + return RedmineApiRequestContent.CreateString(payload, Serializer.ContentType); + } + + protected override object CreateContentFromBytes(byte[] data) + { + return RedmineApiRequestContent.CreateBinary(data); + } + + protected override RedmineApiResponse HandleRequest(string address, string verb, RequestOptions requestOptions = null, object content = null, IProgress progress = null) + { + var requestMessage = CreateRequestMessage(address, verb, Serializer, requestOptions, content as RedmineApiRequestContent); + + var responseMessage = Send(requestMessage, progress); + + return responseMessage; + } + + private static RedmineApiRequest CreateRequestMessage(string address, string verb, IRedmineSerializer serializer, RequestOptions requestOptions = null, RedmineApiRequestContent content = null) + { + var req = new RedmineApiRequest() + { + RequestUri = address, + Method = verb, + }; + + if (requestOptions != null) + { + req.QueryString = requestOptions.QueryString; + req.ImpersonateUser = requestOptions.ImpersonateUser; + if (!requestOptions.Accept.IsNullOrWhiteSpace()) + { + req.Accept = requestOptions.Accept; + } + + if (requestOptions.Headers != null) + { + req.Headers = requestOptions.Headers; + } + + if (!requestOptions.UserAgent.IsNullOrWhiteSpace()) + { + req.UserAgent = requestOptions.UserAgent; + } + } + + if (content != null) + { + req.Content = content; + req.ContentType = content.ContentType; + } + else + { + req.ContentType = serializer.ContentType; + } + + return req; + } + + private RedmineApiResponse Send(RedmineApiRequest requestMessage, IProgress progress = null) + { + System.Net.WebClient webClient = null; + byte[] response = null; + HttpStatusCode? statusCode = null; + NameValueCollection responseHeaders = null; + + try + { + webClient = _webClientFunc(); + + if (requestMessage.QueryString != null) + { + webClient.QueryString = requestMessage.QueryString; + } + + webClient.ApplyHeaders(requestMessage, Credentials); + + if (IsGetOrDownload(requestMessage.Method)) + { + response = requestMessage.Method == HttpConstants.HttpVerbs.DOWNLOAD + ? webClient.DownloadWithProgress(requestMessage.RequestUri, progress) + : webClient.DownloadData(requestMessage.RequestUri); + } + else + { + byte[] payload; + if (requestMessage.Content != null) + { + webClient.Headers.Add(HttpRequestHeader.ContentType, requestMessage.Content.ContentType); + payload = requestMessage.Content.Body; + } + else + { + payload = EmptyBytes; + } + + response = webClient.UploadData(requestMessage.RequestUri, requestMessage.Method, payload); + } + + responseHeaders = webClient.ResponseHeaders; + statusCode = webClient.GetStatusCode(); + } + catch (WebException webException) when (webException.Status == WebExceptionStatus.ProtocolError) + { + HandleResponseException(webException, requestMessage.RequestUri, Serializer); + } + catch (WebException webException) + { + if (webException.Status == WebExceptionStatus.RequestCanceled) + { + throw new RedmineOperationCanceledException(webException.Message, requestMessage.RequestUri, webException.InnerException); + } + + if (webException.Status == WebExceptionStatus.Timeout) + { + throw new RedmineTimeoutException(webException.Message, requestMessage.RequestUri, webException.InnerException); + } + + var errStatusCode = GetExceptionStatusCode(webException); + throw new RedmineApiException(webException.Message, requestMessage.RequestUri, errStatusCode, webException.InnerException); + } + catch (Exception ex) + { + throw new RedmineApiException(ex.Message, requestMessage.RequestUri, HttpConstants.StatusCodes.Unknown, ex.InnerException); + } + finally + { + webClient?.Dispose(); + } + + return new RedmineApiResponse() + { + Headers = responseHeaders, + Content = response, + StatusCode = statusCode ?? HttpStatusCode.OK, + }; + } + + private static void HandleResponseException(WebException exception, string url, IRedmineSerializer serializer) + { + var innerException = exception.InnerException ?? exception; + + if (exception.Response == null) + { + throw new RedmineApiException(exception.Message, url, null, innerException); + } + + var statusCode = GetExceptionStatusCode(exception); + + using var responseStream = exception.Response.GetResponseStream(); + throw statusCode switch + { + HttpConstants.StatusCodes.NotFound => new RedmineNotFoundException(exception.Message, url, innerException), + HttpConstants.StatusCodes.Unauthorized => new RedmineUnauthorizedException(exception.Message, url, innerException), + HttpConstants.StatusCodes.Forbidden => new RedmineForbiddenException(exception.Message, url, innerException), + HttpConstants.StatusCodes.UnprocessableEntity => RedmineExceptionHelper.CreateUnprocessableEntityException(url, responseStream, innerException, serializer), + HttpConstants.StatusCodes.NotAcceptable => new RedmineNotAcceptableException(exception.Message, innerException), + _ => new RedmineApiException(exception.Message, url, statusCode, innerException), + }; + } + + private static int? GetExceptionStatusCode(WebException webException) + { + var statusCode = webException.Response is HttpWebResponse httpResponse + ? (int)httpResponse.StatusCode + : HttpConstants.StatusCodes.Unknown; + return statusCode; + } + } +} diff --git a/src/redmine-net-api/Http/Clients/WebClient/InternalWebClient.cs b/src/redmine-net-api/Http/Clients/WebClient/InternalWebClient.cs new file mode 100644 index 00000000..fabd7219 --- /dev/null +++ b/src/redmine-net-api/Http/Clients/WebClient/InternalWebClient.cs @@ -0,0 +1,135 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Net; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Options; + +namespace Redmine.Net.Api.Http.Clients.WebClient; + +internal sealed class InternalWebClient : System.Net.WebClient +{ + private readonly RedmineWebClientOptions _webClientOptions; + + #pragma warning disable SYSLIB0014 + public InternalWebClient(RedmineManagerOptions redmineManagerOptions) + { + _webClientOptions = redmineManagerOptions.WebClientOptions; + BaseAddress = redmineManagerOptions.BaseAddress.ToString(); + } + #pragma warning restore SYSLIB0014 + + protected override WebRequest GetWebRequest(Uri address) + { + try + { + var webRequest = base.GetWebRequest(address); + + if (webRequest is not HttpWebRequest httpWebRequest) + { + return base.GetWebRequest(address); + } + + httpWebRequest.UserAgent = _webClientOptions.UserAgent; + + AssignIfHasValue(_webClientOptions.DecompressionFormat, value => httpWebRequest.AutomaticDecompression = value); + + AssignIfHasValue(_webClientOptions.AutoRedirect, value => httpWebRequest.AllowAutoRedirect = value); + + AssignIfHasValue(_webClientOptions.MaxAutomaticRedirections, value => httpWebRequest.MaximumAutomaticRedirections = value); + + AssignIfHasValue(_webClientOptions.KeepAlive, value => httpWebRequest.KeepAlive = value); + + AssignIfHasValue(_webClientOptions.Timeout, value => httpWebRequest.Timeout = (int) value.TotalMilliseconds); + + AssignIfHasValue(_webClientOptions.PreAuthenticate, value => httpWebRequest.PreAuthenticate = value); + + AssignIfHasValue(_webClientOptions.UseCookies, value => httpWebRequest.CookieContainer = _webClientOptions.CookieContainer); + + AssignIfHasValue(_webClientOptions.UnsafeAuthenticatedConnectionSharing, value => httpWebRequest.UnsafeAuthenticatedConnectionSharing = value); + + AssignIfHasValue(_webClientOptions.MaxResponseContentBufferSize, value => { }); + + if (_webClientOptions.DefaultHeaders != null) + { + httpWebRequest.Headers = new WebHeaderCollection(); + foreach (var defaultHeader in _webClientOptions.DefaultHeaders) + { + httpWebRequest.Headers.Add(defaultHeader.Key, defaultHeader.Value); + } + } + + httpWebRequest.CachePolicy = _webClientOptions.RequestCachePolicy; + + httpWebRequest.Proxy = _webClientOptions.Proxy; + + httpWebRequest.Credentials = _webClientOptions.Credentials; + + #if NET40_OR_GREATER || NET + if (_webClientOptions.ClientCertificates != null) + { + httpWebRequest.ClientCertificates = _webClientOptions.ClientCertificates; + } + #endif + + #if (NET45_OR_GREATER || NET) + httpWebRequest.ServerCertificateValidationCallback = _webClientOptions.ServerCertificateValidationCallback; + #endif + + if (_webClientOptions.ProtocolVersion != null) + { + httpWebRequest.ProtocolVersion = _webClientOptions.ProtocolVersion; + } + + return httpWebRequest; + } + catch (Exception webException) + { + throw new RedmineException(webException.GetBaseException().Message, webException); + } + } + + public HttpStatusCode StatusCode { get; private set; } + + protected override WebResponse GetWebResponse(WebRequest request) + { + var response = base.GetWebResponse(request); + if (response is HttpWebResponse httpResponse) + { + StatusCode = httpResponse.StatusCode; + } + return response; + } + + protected override WebResponse GetWebResponse(WebRequest request, IAsyncResult result) + { + var response = base.GetWebResponse(request, result); + if (response is HttpWebResponse httpResponse) + { + StatusCode = httpResponse.StatusCode; + } + return response; + } + + private static void AssignIfHasValue(T? nullableValue, Action assignAction) where T : struct + { + if (nullableValue.HasValue) + { + assignAction(nullableValue.Value); + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Http/Clients/WebClient/RedmineApiRequestContent.cs b/src/redmine-net-api/Http/Clients/WebClient/RedmineApiRequestContent.cs new file mode 100644 index 00000000..9d9c33f0 --- /dev/null +++ b/src/redmine-net-api/Http/Clients/WebClient/RedmineApiRequestContent.cs @@ -0,0 +1,93 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Text; +using Redmine.Net.Api.Http.Constants; + +namespace Redmine.Net.Api.Http.Clients.WebClient; + +internal class RedmineApiRequestContent : IDisposable +{ + private static readonly byte[] _emptyByteArray = []; + private bool _isDisposed; + + /// + /// Gets the content type of the request. + /// + public string ContentType { get; } + + /// + /// Gets the body of the request. + /// + public byte[] Body { get; } + + /// + /// Gets the length of the request body. + /// + public int Length => Body?.Length ?? 0; + + /// + /// Creates a new instance of RedmineApiRequestContent. + /// + /// The content type of the request. + /// The body of the request. + /// Thrown when the contentType is null. + public RedmineApiRequestContent(string contentType, byte[] body) + { + ContentType = contentType ?? throw new ArgumentNullException(nameof(contentType)); + Body = body ?? _emptyByteArray; + } + + /// + /// Creates a text-based request content with the specified MIME type. + /// + /// The text content. + /// The MIME type of the content. + /// The encoding to use (defaults to UTF-8). + /// A new RedmineApiRequestContent instance. + public static RedmineApiRequestContent CreateString(string text, string mimeType, Encoding encoding = null) + { + if (string.IsNullOrEmpty(text)) + { + return new RedmineApiRequestContent(mimeType, _emptyByteArray); + } + + encoding ??= Encoding.UTF8; + return new RedmineApiRequestContent(mimeType, encoding.GetBytes(text)); + } + + /// + /// Creates a binary request content. + /// + /// The binary data. + /// A new RedmineApiRequestContent instance. + public static RedmineApiRequestContent CreateBinary(byte[] data) + { + return new RedmineApiRequestContent(HttpConstants.ContentTypes.ApplicationOctetStream, data); + } + + /// + /// Disposes the resources used by this instance. + /// + public void Dispose() + { + if (!_isDisposed) + { + _isDisposed = true; + } + } +} diff --git a/src/redmine-net-api/Http/Clients/WebClient/RedmineWebClientOptions.cs b/src/redmine-net-api/Http/Clients/WebClient/RedmineWebClientOptions.cs new file mode 100644 index 00000000..65291f8e --- /dev/null +++ b/src/redmine-net-api/Http/Clients/WebClient/RedmineWebClientOptions.cs @@ -0,0 +1,82 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System.Net; +#if (NET45_OR_GREATER || NET) +using System.Net.Security; +#endif + +namespace Redmine.Net.Api.Http.Clients.WebClient; +/// +/// +/// +public sealed class RedmineWebClientOptions: RedmineApiClientOptions +{ + + /// + /// + /// + public bool? KeepAlive { get; set; } + + /// + /// + /// + public bool? UnsafeAuthenticatedConnectionSharing { get; set; } + + /// + /// + /// + public int? DefaultConnectionLimit { get; set; } + + /// + /// + /// + public int? DnsRefreshTimeout { get; set; } + + /// + /// + /// + public bool? EnableDnsRoundRobin { get; set; } + + /// + /// + /// + public int? MaxServicePoints { get; set; } + + /// + /// + /// + public int? MaxServicePointIdleTime { get; set; } + + #if(NET46_OR_GREATER || NET) + /// + /// + /// + public bool? ReusePort { get; set; } + #endif + + /// + /// + /// + public SecurityProtocolType? SecurityProtocolType { get; set; } + +#if (NET45_OR_GREATER || NET) + /// + /// + /// + public RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; } + #endif +} \ No newline at end of file diff --git a/src/redmine-net-api/Http/Clients/WebClient/WebClientExtensions.cs b/src/redmine-net-api/Http/Clients/WebClient/WebClientExtensions.cs new file mode 100644 index 00000000..3a831655 --- /dev/null +++ b/src/redmine-net-api/Http/Clients/WebClient/WebClientExtensions.cs @@ -0,0 +1,110 @@ +using System; +using System.Globalization; +using System.Net; +using Redmine.Net.Api.Authentication; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http.Helpers; +using Redmine.Net.Api.Http.Messages; + +namespace Redmine.Net.Api.Http.Clients.WebClient; + +internal static class WebClientExtensions +{ + public static void ApplyHeaders(this System.Net.WebClient client, RedmineApiRequest request, IRedmineAuthentication authentication) + { + switch (authentication) + { + case RedmineApiKeyAuthentication: + client.Headers.Add(RedmineConstants.API_KEY_AUTHORIZATION_HEADER_KEY, authentication.Token); + break; + case RedmineBasicAuthentication: + client.Headers.Add(RedmineConstants.AUTHORIZATION_HEADER_KEY, authentication.Token); + break; + } + + client.Headers.Add(RedmineConstants.CONTENT_TYPE_HEADER_KEY, request.ContentType); + + if (!request.UserAgent.IsNullOrWhiteSpace()) + { + client.Headers.Add(RedmineConstants.USER_AGENT_HEADER_KEY, request.UserAgent); + } + + if (!request.ImpersonateUser.IsNullOrWhiteSpace()) + { + client.Headers.Add(RedmineConstants.IMPERSONATE_HEADER_KEY, request.ImpersonateUser); + } + + if (request.Headers is not { Count: > 0 }) + { + return; + } + + foreach (var header in request.Headers) + { + client.Headers.Add(header.Key, header.Value); + } + + if (!request.Accept.IsNullOrWhiteSpace()) + { + client.Headers.Add(HttpRequestHeader.Accept, request.Accept); + } + } + + internal static byte[] DownloadWithProgress(this System.Net.WebClient webClient, string url, IProgress progress) + { + var contentLength = GetContentLength(webClient); + byte[] data; + if (contentLength > 0) + { + using (var respStream = webClient.OpenRead(url)) + { + data = new byte[contentLength]; + var buffer = new byte[4096]; + int bytesRead; + var totalBytesRead = 0; + + while ((bytesRead = respStream.Read(buffer, 0, buffer.Length)) > 0) + { + Buffer.BlockCopy(buffer, 0, data, totalBytesRead, bytesRead); + totalBytesRead += bytesRead; + + ClientHelper.ReportProgress(progress, contentLength, totalBytesRead); + } + } + } + else + { + data = webClient.DownloadData(url); + progress?.Report(100); + } + + return data; + } + + internal static long GetContentLength(this System.Net.WebClient webClient) + { + var total = -1L; + if (webClient.ResponseHeaders == null) + { + return total; + } + + var contentLengthAsString = webClient.ResponseHeaders[HttpRequestHeader.ContentLength]; + if (!string.IsNullOrEmpty(contentLengthAsString)) + { + total = Convert.ToInt64(contentLengthAsString, CultureInfo.InvariantCulture); + } + + return total; + } + + internal static HttpStatusCode? GetStatusCode(this System.Net.WebClient webClient) + { + if (webClient is InternalWebClient iwc) + { + return iwc.StatusCode; + } + + return null; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Http/Clients/WebClient/WebClientProvider.cs b/src/redmine-net-api/Http/Clients/WebClient/WebClientProvider.cs new file mode 100644 index 00000000..10af757d --- /dev/null +++ b/src/redmine-net-api/Http/Clients/WebClient/WebClientProvider.cs @@ -0,0 +1,65 @@ +using System.Text; +using Redmine.Net.Api.Options; + +namespace Redmine.Net.Api.Http.Clients.WebClient; + +internal static class WebClientProvider +{ + /// + /// Creates a new WebClient instance with the specified options. + /// + /// The options for the Redmine manager. + /// A new WebClient instance. + public static System.Net.WebClient CreateWebClient(RedmineManagerOptions options) + { + var webClient = new InternalWebClient(options); + + if (options?.ApiClientOptions is RedmineWebClientOptions webClientOptions) + { + ConfigureWebClient(webClient, webClientOptions); + } + + return webClient; + } + + /// + /// Configures a WebClient instance with the specified options. + /// + /// The WebClient instance to configure. + /// The options to apply. + private static void ConfigureWebClient(System.Net.WebClient webClient, RedmineWebClientOptions options) + { + if (options == null) return; + + webClient.Proxy = options.Proxy; + webClient.Headers = null; + webClient.BaseAddress = null; + webClient.CachePolicy = null; + webClient.Credentials = null; + webClient.Encoding = Encoding.UTF8; + webClient.UseDefaultCredentials = false; + + // if (options.Timeout.HasValue && options.Timeout.Value != TimeSpan.Zero) + // { + // webClient.Timeout = options.Timeout; + // } + // + // if (options.KeepAlive.HasValue) + // { + // webClient.KeepAlive = options.KeepAlive.Value; + // } + // + // if (options.UnsafeAuthenticatedConnectionSharing.HasValue) + // { + // webClient.UnsafeAuthenticatedConnectionSharing = options.UnsafeAuthenticatedConnectionSharing.Value; + // } + // + // #if NET40_OR_GREATER || NET + // if (options.ClientCertificates != null) + // { + // webClient.ClientCertificates = options.ClientCertificates; + // } + // #endif + + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Http/Constants/HttpConstants.cs b/src/redmine-net-api/Http/Constants/HttpConstants.cs new file mode 100644 index 00000000..77ad1f4c --- /dev/null +++ b/src/redmine-net-api/Http/Constants/HttpConstants.cs @@ -0,0 +1,108 @@ +namespace Redmine.Net.Api.Http.Constants; + +/// +/// +/// +public static class HttpConstants +{ + /// + /// HTTP status codes including custom codes used by Redmine. + /// + internal static class StatusCodes + { + public const int Unauthorized = 401; + public const int Forbidden = 403; + public const int NotFound = 404; + public const int NotAcceptable = 406; + public const int RequestTimeout = 408; + public const int Conflict = 409; + public const int UnprocessableEntity = 422; + public const int TooManyRequests = 429; + public const int ClientCloseRequest = 499; + public const int InternalServerError = 500; + public const int BadGateway = 502; + public const int ServiceUnavailable = 503; + public const int GatewayTimeout = 504; + public const int Unknown = 999; + } + + /// + /// Standard HTTP headers used in API requests and responses. + /// + internal static class Headers + { + public const string Authorization = "Authorization"; + public const string ApiKey = "X-Redmine-API-Key"; + public const string Impersonate = "X-Redmine-Switch-User"; + public const string ContentType = "Content-Type"; + } + + internal static class Names + { + /// HTTP User-Agent header name. + public static string UserAgent => "User-Agent"; + } + + internal static class Values + { + /// User agent string to use for all HTTP requests. + public static string UserAgent => "Redmine-NET-API"; + } + + /// + /// MIME content types used in API requests and responses. + /// + internal static class ContentTypes + { + public const string ApplicationJson = "application/json"; + public const string ApplicationXml = "application/xml"; + public const string ApplicationOctetStream = "application/octet-stream"; + } + + /// + /// Error messages for different HTTP status codes. + /// + internal static class ErrorMessages + { + public const string NotFound = "The requested resource was not found."; + public const string Unauthorized = "Authentication is required or has failed."; + public const string Forbidden = "You don't have permission to access this resource."; + public const string Conflict = "The resource you are trying to update has been modified since you last retrieved it."; + public const string NotAcceptable = "The requested format is not supported."; + public const string InternalServerError = "The server encountered an unexpected error."; + public const string UnprocessableEntity = "Validation failed for the submitted data."; + public const string Cancelled = "The operation was cancelled."; + public const string TimedOut = "The operation has timed out."; + } + + /// + /// + /// + internal static class HttpVerbs + { + /// + /// Represents an HTTP GET protocol method that is used to get an entity identified by a URI. + /// + public const string GET = "GET"; + /// + /// Represents an HTTP PUT protocol method that is used to replace an entity identified by a URI. + /// + public const string PUT = "PUT"; + /// + /// Represents an HTTP POST protocol method that is used to post a new entity as an addition to a URI. + /// + public const string POST = "POST"; + /// + /// Represents an HTTP PATCH protocol method that is used to patch an existing entity identified by a URI. + /// + public const string PATCH = "PATCH"; + /// + /// Represents an HTTP DELETE protocol method that is used to delete an existing entity identified by a URI. + /// + public const string DELETE = "DELETE"; + + internal const string DOWNLOAD = "DOWNLOAD"; + + internal const string UPLOAD = "UPLOAD"; + } +} diff --git a/src/redmine-net-api/Http/Extensions/NameValueCollectionExtensions.cs b/src/redmine-net-api/Http/Extensions/NameValueCollectionExtensions.cs new file mode 100644 index 00000000..5199775f --- /dev/null +++ b/src/redmine-net-api/Http/Extensions/NameValueCollectionExtensions.cs @@ -0,0 +1,267 @@ +ο»Ώ/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.Text; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.Http.Extensions +{ + /// + /// + /// + public static class NameValueCollectionExtensions + { + /// + /// Gets the parameter value. + /// + /// The parameters. + /// Name of the parameter. + /// + public static string GetParameterValue(this NameValueCollection parameters, string parameterName) + { + return GetValue(parameters, parameterName); + } + + /// + /// Gets the parameter value. + /// + /// The parameters. + /// Name of the parameter. + /// + public static string GetValue(this NameValueCollection parameters, string key) + { + if (parameters == null) + { + return null; + } + + var value = parameters.Get(key); + + return value.IsNullOrWhiteSpace() ? null : value; + } + + /// + /// + /// + /// + /// + public static string ToQueryString(this NameValueCollection requestParameters) + { + if (requestParameters == null || requestParameters.Count == 0) + { + return null; + } + + var delimiter = string.Empty; + + var stringBuilder = new StringBuilder(); + + for (var index = 0; index < requestParameters.Count; ++index) + { + stringBuilder + .Append(delimiter) + .Append(requestParameters.AllKeys[index].ToString(CultureInfo.InvariantCulture)) + .Append('=') + .Append(requestParameters[index].ToString(CultureInfo.InvariantCulture)); + delimiter = "&"; + } + + var queryString = stringBuilder.ToString(); + + stringBuilder.Length = 0; + + return queryString; + } + + internal static NameValueCollection AddPagingParameters(this NameValueCollection parameters, int pageSize, int offset) + { + parameters ??= new NameValueCollection(); + + if(pageSize <= 0) + { + pageSize = RedmineConstants.DEFAULT_PAGE_SIZE_VALUE; + } + + if(offset < 0) + { + offset = 0; + } + + parameters.Set(RedmineKeys.LIMIT, pageSize.ToInvariantString()); + parameters.Set(RedmineKeys.OFFSET, offset.ToInvariantString()); + + return parameters; + } + + internal static NameValueCollection AddParamsIfExist(this NameValueCollection parameters, string[] include) + { + if (include is not {Length: > 0}) + { + return parameters; + } + + parameters ??= new NameValueCollection(); + + parameters.Add(RedmineKeys.INCLUDE, string.Join(",", include)); + + return parameters; + } + + internal static void AddIfNotNull(this NameValueCollection nameValueCollection, string key, string value) + { + if (!value.IsNullOrWhiteSpace()) + { + nameValueCollection.Add(key, value); + } + } + + internal static void AddIfNotNull(this NameValueCollection nameValueCollection, string key, bool? value) + { + if (value.HasValue) + { + nameValueCollection.Add(key, value.Value.ToInvariantString()); + } + } + + /// + /// Creates a new NameValueCollection with an initial key-value pair. + /// + /// The key for the first item. + /// The value for the first item. + /// A new NameValueCollection containing the specified key-value pair. + public static NameValueCollection WithItem(this string key, string value) + { + var collection = new NameValueCollection(); + collection.Add(key, value); + return collection; + } + + /// + /// Adds a new key-value pair to an existing NameValueCollection and returns the collection for chaining. + /// + /// The NameValueCollection to add to. + /// The key to add. + /// The value to add. + /// The NameValueCollection with the new key-value pair added. + public static NameValueCollection AndItem(this NameValueCollection collection, string key, string value) + { + collection.Add(key, value); + return collection; + } + + /// + /// Adds a new key-value pair to an existing NameValueCollection if the condition is true. + /// + /// The NameValueCollection to add to. + /// The condition to evaluate. + /// The key to add if condition is true. + /// The value to add if condition is true. + /// The NameValueCollection, potentially with a new key-value pair added. + public static NameValueCollection AndItemIf(this NameValueCollection collection, bool condition, string key, string value) + { + if (condition) + { + collection.Add(key, value); + } + return collection; + } + + /// + /// Adds a new key-value pair to an existing NameValueCollection if the value is not null. + /// + /// The NameValueCollection to add to. + /// The key to add if value is not null. + /// The value to check and add. + /// The NameValueCollection, potentially with a new key-value pair added. + public static NameValueCollection AndItemIfNotNull(this NameValueCollection collection, string key, string value) + { + if (value != null) + { + collection.Add(key, value); + } + return collection; + } + + /// + /// Creates a new NameValueCollection with an initial key-value pair where the value is converted from an integer. + /// + /// The key for the first item. + /// The integer value to be converted to string. + /// A new NameValueCollection containing the specified key-value pair. + public static NameValueCollection WithInt(this string key, int value) + { + return key.WithItem(value.ToInvariantString()); + } + + /// + /// Adds a new key-value pair to an existing NameValueCollection where the value is converted from an integer. + /// + /// The NameValueCollection to add to. + /// The key to add. + /// The integer value to be converted to string. + /// The NameValueCollection with the new key-value pair added. + public static NameValueCollection AndInt(this NameValueCollection collection, string key, int value) + { + return collection.AndItem(key, value.ToInvariantString()); + } + + + /// + /// Converts a NameValueCollection to a Dictionary. + /// + /// The collection to convert. + /// A new Dictionary containing the collection's key-value pairs. + public static Dictionary ToDictionary(this NameValueCollection collection) + { + var dict = new Dictionary(); + + if (collection != null) + { + foreach (string key in collection.Keys) + { + dict[key] = collection[key]; + } + } + + return dict; + } + + /// + /// Creates a new NameValueCollection from a dictionary of key-value pairs. + /// + /// Dictionary of key-value pairs to add to the collection. + /// A new NameValueCollection containing the specified key-value pairs. + public static NameValueCollection ToNameValueCollection(this Dictionary keyValuePairs) + { + var collection = new NameValueCollection(); + + if (keyValuePairs != null) + { + foreach (var pair in keyValuePairs) + { + collection.Add(pair.Key, pair.Value); + } + } + + return collection; + } + + + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Http/Extensions/RedmineApiResponseExtensions.cs b/src/redmine-net-api/Http/Extensions/RedmineApiResponseExtensions.cs new file mode 100644 index 00000000..e03d3ed5 --- /dev/null +++ b/src/redmine-net-api/Http/Extensions/RedmineApiResponseExtensions.cs @@ -0,0 +1,55 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System.Collections.Generic; +using System.Text; +using Redmine.Net.Api.Common; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http.Messages; +using Redmine.Net.Api.Serialization; + +namespace Redmine.Net.Api.Http.Extensions; + +internal static class RedmineApiResponseExtensions +{ + internal static T DeserializeTo(this RedmineApiResponse responseMessage, IRedmineSerializer redmineSerializer) where T : new() + { + var responseAsString = GetResponseContentAsString(responseMessage); + return responseAsString.IsNullOrWhiteSpace() ? default : redmineSerializer.Deserialize(responseAsString); + } + + internal static PagedResults DeserializeToPagedResults(this RedmineApiResponse responseMessage, IRedmineSerializer redmineSerializer) where T : class, new() + { + var responseAsString = GetResponseContentAsString(responseMessage); + return responseAsString.IsNullOrWhiteSpace() ? default : redmineSerializer.DeserializeToPagedResults(responseAsString); + } + + internal static List DeserializeToList(this RedmineApiResponse responseMessage, IRedmineSerializer redmineSerializer) where T : class, new() + { + var responseAsString = GetResponseContentAsString(responseMessage); + return responseAsString.IsNullOrWhiteSpace() ? null : redmineSerializer.Deserialize>(responseAsString); + } + + /// + /// Gets the response content as a UTF-8 encoded string. + /// + /// The API response message. + /// The content as a string, or null if the response or content is null. + private static string GetResponseContentAsString(RedmineApiResponse responseMessage) + { + return responseMessage?.Content == null ? null : Encoding.UTF8.GetString(responseMessage.Content); + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Http/Helpers/ClientHelper.cs b/src/redmine-net-api/Http/Helpers/ClientHelper.cs new file mode 100644 index 00000000..6ffbef3b --- /dev/null +++ b/src/redmine-net-api/Http/Helpers/ClientHelper.cs @@ -0,0 +1,16 @@ +using System; + +namespace Redmine.Net.Api.Http.Helpers; + +internal static class ClientHelper +{ + internal static void ReportProgress(IProgressprogress, long total, long bytesRead) + { + if (progress == null || total <= 0) + { + return; + } + var percent = (int)(bytesRead * 100L / total); + progress.Report(percent); + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Http/Helpers/RedmineExceptionHelper.cs b/src/redmine-net-api/Http/Helpers/RedmineExceptionHelper.cs new file mode 100644 index 00000000..2d51e16e --- /dev/null +++ b/src/redmine-net-api/Http/Helpers/RedmineExceptionHelper.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http.Constants; +using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.Http.Helpers; + +/// +/// Handles HTTP status codes and converts them to appropriate Redmine exceptions. +/// +internal static class RedmineExceptionHelper +{ + /// + /// Creates an exception for a 422 Unprocessable Entity response. + /// + /// + /// The response stream containing error details. + /// The inner exception, if any. + /// The serializer to use for deserializing error messages. + /// A RedmineApiException with details about the validation errors. + internal static RedmineApiException CreateUnprocessableEntityException(string uri, Stream responseStream, Exception inner, IRedmineSerializer serializer) + { + var errors = GetRedmineErrors(responseStream, serializer); + + if (errors is null) + { + return new RedmineUnprocessableEntityException(HttpConstants.ErrorMessages.UnprocessableEntity ,uri, inner); + } + + var message = BuildUnprocessableContentMessage(errors); + + return new RedmineUnprocessableEntityException(message, url: uri, inner); + } + + internal static RedmineApiException CreateUnprocessableEntityException(string url, string content, IRedmineSerializer serializer) + { + var paged = serializer.DeserializeToPagedResults(content); + + var message = BuildUnprocessableContentMessage(paged.Items); + + return new RedmineApiException(message: message, url: url, httpStatusCode: HttpConstants.StatusCodes.UnprocessableEntity, innerException: null); + } + + internal static string BuildUnprocessableContentMessage(List errors) + { + var sb = new StringBuilder(); + foreach (var error in errors) + { + sb.Append(error.Info); + sb.Append(Environment.NewLine); + } + + if (sb.Length > 0) + { + sb.Length -= 1; + } + + return sb.ToString(); + } + + /// + /// Gets the Redmine errors from a response stream. + /// + /// The response stream containing error details. + /// The serializer to use for deserializing error messages. + /// A list of error objects or null if unable to parse errors. + private static List GetRedmineErrors(Stream responseStream, IRedmineSerializer serializer) + { + if (responseStream == null) + { + return null; + } + + using (responseStream) + { + using var reader = new StreamReader(responseStream); + var content = reader.ReadToEnd(); + if (content.IsNullOrWhiteSpace()) + { + return null; + } + + var paged = serializer.DeserializeToPagedResults(content); + return paged.Items; + } + } +} diff --git a/src/redmine-net-api/Http/Helpers/RedmineHttpMethodHelper.cs b/src/redmine-net-api/Http/Helpers/RedmineHttpMethodHelper.cs new file mode 100644 index 00000000..6adee604 --- /dev/null +++ b/src/redmine-net-api/Http/Helpers/RedmineHttpMethodHelper.cs @@ -0,0 +1,63 @@ +using System; +using Redmine.Net.Api.Http.Constants; +#if !NET20 +using System.Net.Http; +#endif + +namespace Redmine.Net.Api.Http.Helpers; + +internal static class RedmineHttpMethodHelper +{ +#if !NET20 + private static readonly HttpMethod PatchMethod = new HttpMethod("PATCH"); + private static readonly HttpMethod DownloadMethod = new HttpMethod("DOWNLOAD"); + + /// + /// Gets an HttpMethod instance for the specified HTTP verb. + /// + /// The HTTP verb (GET, POST, etc.). + /// An HttpMethod instance corresponding to the verb. + /// Thrown when the verb is not supported. + public static HttpMethod GetHttpMethod(string verb) + { + return verb switch + { + HttpConstants.HttpVerbs.GET => HttpMethod.Get, + HttpConstants.HttpVerbs.POST => HttpMethod.Post, + HttpConstants.HttpVerbs.PUT => HttpMethod.Put, + HttpConstants.HttpVerbs.PATCH => PatchMethod, + HttpConstants.HttpVerbs.DELETE => HttpMethod.Delete, + HttpConstants.HttpVerbs.DOWNLOAD => DownloadMethod, + _ => throw new ArgumentException($"Unsupported HTTP verb: {verb}") + }; + } +#endif + /// + /// Determines whether the specified HTTP method is a GET or DOWNLOAD method. + /// + /// The HTTP method to check. + /// True if the method is GET or DOWNLOAD; otherwise, false. + public static bool IsGetOrDownload(string method) + { + return method == HttpConstants.HttpVerbs.GET || method == HttpConstants.HttpVerbs.DOWNLOAD; + } + + /// + /// Determines whether the HTTP status code represents a transient error. + /// + /// The HTTP response status code. + /// True if the status code represents a transient error; otherwise, false. + internal static bool IsTransientError(int statusCode) + { + return statusCode switch + { + HttpConstants.StatusCodes.BadGateway => true, + HttpConstants.StatusCodes.GatewayTimeout => true, + HttpConstants.StatusCodes.ServiceUnavailable => true, + HttpConstants.StatusCodes.RequestTimeout => true, + HttpConstants.StatusCodes.TooManyRequests => true, + _ => false + }; + } + +} diff --git a/src/redmine-net-api/Http/IRedmineApiClient.cs b/src/redmine-net-api/Http/IRedmineApiClient.cs new file mode 100644 index 00000000..ed3b23c9 --- /dev/null +++ b/src/redmine-net-api/Http/IRedmineApiClient.cs @@ -0,0 +1,73 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using Redmine.Net.Api.Http.Messages; +#if !NET20 +using System.Threading; +using System.Threading.Tasks; +#endif + +namespace Redmine.Net.Api.Http; +/// +/// +/// +internal interface IRedmineApiClient : ISyncRedmineApiClient +#if !NET20 + , IAsyncRedmineApiClient +#endif +{ +} + +internal interface ISyncRedmineApiClient +{ + RedmineApiResponse Get(string address, RequestOptions requestOptions = null); + + RedmineApiResponse GetPaged(string address, RequestOptions requestOptions = null); + + RedmineApiResponse Create(string address, string payload, RequestOptions requestOptions = null); + + RedmineApiResponse Update(string address, string payload, RequestOptions requestOptions = null); + + RedmineApiResponse Patch(string address, string payload, RequestOptions requestOptions = null); + + RedmineApiResponse Delete(string address, RequestOptions requestOptions = null); + + RedmineApiResponse Upload(string address, byte[] data, RequestOptions requestOptions = null); + + RedmineApiResponse Download(string address, RequestOptions requestOptions = null, IProgress progress = null); +} + +#if !NET20 +internal interface IAsyncRedmineApiClient +{ + Task GetAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + + Task GetPagedAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + + Task CreateAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + + Task UpdateAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + + Task PatchAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + + Task DeleteAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + + Task UploadFileAsync(string address, byte[] data, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + + Task DownloadAsync(string address, RequestOptions requestOptions = null, IProgress progress = null, CancellationToken cancellationToken = default); +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Http/IRedmineApiClientOptions.cs b/src/redmine-net-api/Http/IRedmineApiClientOptions.cs new file mode 100644 index 00000000..df00aaef --- /dev/null +++ b/src/redmine-net-api/Http/IRedmineApiClientOptions.cs @@ -0,0 +1,147 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Cache; +using System.Security.Cryptography.X509Certificates; + +namespace Redmine.Net.Api.Http +{ + /// + /// + /// + public interface IRedmineApiClientOptions + { + /// + /// + /// + bool? AutoRedirect { get; set; } + + /// + /// + /// + CookieContainer CookieContainer { get; set; } + + /// + /// + /// + DecompressionMethods? DecompressionFormat { get; set; } + + /// + /// + /// + ICredentials Credentials { get; set; } + + /// + /// + /// + Dictionary DefaultHeaders { get; set; } + + /// + /// + /// + IWebProxy Proxy { get; set; } + + /// + /// + /// + int? MaxAutomaticRedirections { get; set; } + +#if NET471_OR_GREATER || NET + /// + /// + /// + int? MaxConnectionsPerServer { get; set; } + + /// + /// + /// + int? MaxResponseHeadersLength { get; set; } +#endif + /// + /// + /// + bool? PreAuthenticate { get; set; } + + /// + /// + /// + RequestCachePolicy RequestCachePolicy { get; set; } + + /// + /// + /// + string Scheme { get; set; } + + /// + /// + /// + TimeSpan? Timeout { get; set; } + + /// + /// + /// + string UserAgent { get; set; } + + /// + /// + /// + bool? UseCookies { get; set; } + +#if NETFRAMEWORK + + /// + /// + /// + bool CheckCertificateRevocationList { get; set; } + + /// + /// + /// + long? MaxRequestContentBufferSize { get; set; } + + /// + /// + /// + long? MaxResponseContentBufferSize { get; set; } + + /// + /// + /// + bool? UseDefaultCredentials { get; set; } +#endif + /// + /// + /// + bool? UseProxy { get; set; } + + /// + /// + /// + /// Only HTTP/1.0 and HTTP/1.1 version requests are currently supported. + Version ProtocolVersion { get; set; } + + +#if NET40_OR_GREATER || NET + /// + /// + /// + public X509CertificateCollection ClientCertificates { get; set; } +#endif + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Http/Messages/RedmineApiRequest.cs b/src/redmine-net-api/Http/Messages/RedmineApiRequest.cs new file mode 100644 index 00000000..bce2a22f --- /dev/null +++ b/src/redmine-net-api/Http/Messages/RedmineApiRequest.cs @@ -0,0 +1,68 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System.Collections.Generic; +using System.Collections.Specialized; +using Redmine.Net.Api.Http.Clients.WebClient; +using Redmine.Net.Api.Http.Constants; + +namespace Redmine.Net.Api.Http.Messages; + +internal sealed class RedmineApiRequest +{ + /// + /// + /// + public RedmineApiRequestContent Content { get; set; } + + /// + /// + /// + public string Method { get; set; } = HttpConstants.HttpVerbs.GET; + + /// + /// + /// + public string RequestUri { get; set; } + + /// + /// + /// + public NameValueCollection QueryString { get; set; } + /// + /// + /// + public string ImpersonateUser { get; set; } + + /// + /// + /// + public string ContentType { get; set; } + + /// + /// + /// + public string Accept { get; set; } + /// + /// + /// + public string UserAgent { get; set; } + + /// + /// + /// + public Dictionary Headers { get; set; } +} \ No newline at end of file diff --git a/redmine-net40-api-signed/Attachments.cs b/src/redmine-net-api/Http/Messages/RedmineApiResponse.cs similarity index 63% rename from redmine-net40-api-signed/Attachments.cs rename to src/redmine-net-api/Http/Messages/RedmineApiResponse.cs index 2cd138df..71fb1948 100644 --- a/redmine-net40-api-signed/Attachments.cs +++ b/src/redmine-net-api/Http/Messages/RedmineApiResponse.cs @@ -1,5 +1,5 @@ /* - Copyright 2016 - 2017 Adrian Popescu. + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,13 +14,16 @@ You may obtain a copy of the License at limitations under the License. */ -using System.Collections.Generic; -using Redmine.Net.Api.Types; +using System.Collections.Specialized; +using System.Net; -namespace Redmine.Net.Api +namespace Redmine.Net.Api.Http.Messages; + +internal sealed class RedmineApiResponse { - public class Attachments : Dictionary - { + public NameValueCollection Headers { get; init; } + public byte[] Content { get; init; } + + public HttpStatusCode StatusCode { get; init; } - } } \ No newline at end of file diff --git a/redmine-net20-api/Logging/ILogger.cs b/src/redmine-net-api/Http/RedirectType.cs old mode 100755 new mode 100644 similarity index 70% rename from redmine-net20-api/Logging/ILogger.cs rename to src/redmine-net-api/Http/RedirectType.cs index 79887d64..5bb7acf7 --- a/redmine-net20-api/Logging/ILogger.cs +++ b/src/redmine-net-api/Http/RedirectType.cs @@ -1,5 +1,5 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. +/* + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,17 +14,24 @@ You may obtain a copy of the License at limitations under the License. */ -namespace Redmine.Net.Api.Logging +namespace Redmine.Net.Api.Http { /// /// /// - public interface ILogger + internal enum RedirectType { /// - /// Logs the specified entry. + /// /// - /// The entry. - void Log(LogEntry entry); + None, + /// + /// + /// + OnlyHost, + /// + /// + /// + All } } \ No newline at end of file diff --git a/src/redmine-net-api/Http/RedmineApiClient.Async.cs b/src/redmine-net-api/Http/RedmineApiClient.Async.cs new file mode 100644 index 00000000..e30fb302 --- /dev/null +++ b/src/redmine-net-api/Http/RedmineApiClient.Async.cs @@ -0,0 +1,77 @@ +#if !NET20 +using System; +using System.Threading; +using System.Threading.Tasks; +using Redmine.Net.Api.Http.Constants; +using Redmine.Net.Api.Http.Messages; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Net.Internal; + +namespace Redmine.Net.Api.Http; + +internal abstract partial class RedmineApiClient +{ + public async Task GetAsync(string address, RequestOptions requestOptions = null, + CancellationToken cancellationToken = default) + { + return await HandleRequestAsync(address, HttpConstants.HttpVerbs.GET, requestOptions, cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + + public async Task GetPagedAsync(string address, RequestOptions requestOptions = null, + CancellationToken cancellationToken = default) + { + return await GetAsync(address, requestOptions, cancellationToken).ConfigureAwait(false); + } + + public async Task CreateAsync(string address, string payload, + RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + return await HandleRequestAsync(address, HttpConstants.HttpVerbs.POST, requestOptions, CreateContentFromPayload(payload), + cancellationToken: cancellationToken).ConfigureAwait(false); + } + + public async Task UpdateAsync(string address, string payload, + RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + return await HandleRequestAsync(address, HttpConstants.HttpVerbs.PUT, requestOptions, CreateContentFromPayload(payload), + cancellationToken: cancellationToken).ConfigureAwait(false); + } + + public async Task UploadFileAsync(string address, byte[] data, + RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + return await HandleRequestAsync(address, HttpConstants.HttpVerbs.POST, requestOptions, CreateContentFromBytes(data), + cancellationToken: cancellationToken).ConfigureAwait(false); + } + + public async Task PatchAsync(string address, string payload, + RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + return await HandleRequestAsync(address, HttpConstants.HttpVerbs.PATCH, requestOptions, CreateContentFromPayload(payload), + cancellationToken: cancellationToken).ConfigureAwait(false); + } + + public async Task DeleteAsync(string address, RequestOptions requestOptions = null, + CancellationToken cancellationToken = default) + { + return await HandleRequestAsync(address, HttpConstants.HttpVerbs.DELETE, requestOptions, cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + + public async Task DownloadAsync(string address, RequestOptions requestOptions = null, + IProgress progress = null, CancellationToken cancellationToken = default) + { + return await HandleRequestAsync(address, HttpConstants.HttpVerbs.DOWNLOAD, requestOptions, progress: progress, + cancellationToken: cancellationToken).ConfigureAwait(false); + } + + protected abstract Task HandleRequestAsync( + string address, + string verb, + RequestOptions requestOptions = null, + object content = null, + IProgress progress = null, + CancellationToken cancellationToken = default); +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Http/RedmineApiClient.cs b/src/redmine-net-api/Http/RedmineApiClient.cs new file mode 100644 index 00000000..93b56aef --- /dev/null +++ b/src/redmine-net-api/Http/RedmineApiClient.cs @@ -0,0 +1,106 @@ +using System; +using System.Net; +using Redmine.Net.Api.Authentication; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http.Constants; +using Redmine.Net.Api.Http.Extensions; +using Redmine.Net.Api.Http.Messages; +using Redmine.Net.Api.Logging; +using Redmine.Net.Api.Options; +using Redmine.Net.Api.Serialization; + +namespace Redmine.Net.Api.Http; + +internal abstract partial class RedmineApiClient : IRedmineApiClient +{ + protected readonly IRedmineAuthentication Credentials; + protected readonly IRedmineSerializer Serializer; + protected readonly RedmineManagerOptions Options; + + protected RedmineApiClient(RedmineManagerOptions redmineManagerOptions) + { + Credentials = redmineManagerOptions.Authentication; + Serializer = redmineManagerOptions.Serializer; + Options = redmineManagerOptions; + } + + public RedmineApiResponse Get(string address, RequestOptions requestOptions = null) + { + return HandleRequest(address, HttpConstants.HttpVerbs.GET, requestOptions); + } + + public RedmineApiResponse GetPaged(string address, RequestOptions requestOptions = null) + { + return Get(address, requestOptions); + } + + public RedmineApiResponse Create(string address, string payload, RequestOptions requestOptions = null) + { + return HandleRequest(address, HttpConstants.HttpVerbs.POST, requestOptions, CreateContentFromPayload(payload)); + } + + public RedmineApiResponse Update(string address, string payload, RequestOptions requestOptions = null) + { + return HandleRequest(address, HttpConstants.HttpVerbs.PUT, requestOptions, CreateContentFromPayload(payload)); + } + + public RedmineApiResponse Patch(string address, string payload, RequestOptions requestOptions = null) + { + return HandleRequest(address, HttpConstants.HttpVerbs.PATCH, requestOptions, CreateContentFromPayload(payload)); + } + + public RedmineApiResponse Delete(string address, RequestOptions requestOptions = null) + { + return HandleRequest(address, HttpConstants.HttpVerbs.DELETE, requestOptions); + } + + public RedmineApiResponse Download(string address, RequestOptions requestOptions = null, + IProgress progress = null) + { + return HandleRequest(address, HttpConstants.HttpVerbs.DOWNLOAD, requestOptions, progress: progress); + } + + public RedmineApiResponse Upload(string address, byte[] data, RequestOptions requestOptions = null) + { + return HandleRequest(address, HttpConstants.HttpVerbs.POST, requestOptions, CreateContentFromBytes(data)); + } + + protected abstract RedmineApiResponse HandleRequest( + string address, + string verb, + RequestOptions requestOptions = null, + object content = null, + IProgress progress = null); + + protected abstract object CreateContentFromPayload(string payload); + + protected abstract object CreateContentFromBytes(byte[] data); + + protected static bool IsGetOrDownload(string method) + { + return method is HttpConstants.HttpVerbs.GET or HttpConstants.HttpVerbs.DOWNLOAD; + } + + protected void LogRequest(string verb, string address, RequestOptions requestOptions) + { + if (Options.LoggingOptions?.IncludeHttpDetails != true) + { + return; + } + + Options.Logger.Info($"Request HTTP {verb} {address}"); + + if (requestOptions?.QueryString != null) + { + Options.Logger.Info($"Query parameters: {requestOptions.QueryString.ToQueryString()}"); + } + } + + protected void LogResponse(int statusCode) + { + if (Options.LoggingOptions?.IncludeHttpDetails == true) + { + Options.Logger.Info($"Response status: {statusCode.ToInvariantString()}"); + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Http/RedmineApiClientOptions.cs b/src/redmine-net-api/Http/RedmineApiClientOptions.cs new file mode 100644 index 00000000..7aa7f4e0 --- /dev/null +++ b/src/redmine-net-api/Http/RedmineApiClientOptions.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Cache; +#if NET || NET471_OR_GREATER +using System.Net.Http; +#endif +using System.Security.Cryptography.X509Certificates; + +namespace Redmine.Net.Api.Http; + +/// +/// +/// +public abstract class RedmineApiClientOptions : IRedmineApiClientOptions +{ + /// + /// + /// + public bool? AutoRedirect { get; set; } + + /// + /// + /// + public CookieContainer CookieContainer { get; set; } + + /// + /// + /// + public DecompressionMethods? DecompressionFormat { get; set; } = +#if NET + DecompressionMethods.All; +#else + DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.None; +#endif + + /// + /// + /// + public ICredentials Credentials { get; set; } + + /// + /// + /// + public Dictionary DefaultHeaders { get; set; } + + /// + /// + /// + public IWebProxy Proxy { get; set; } + + /// + /// + /// + public int? MaxAutomaticRedirections { get; set; } + + + + /// + /// + /// + public long? MaxResponseContentBufferSize { get; set; } + + /// + /// + /// + public int? MaxConnectionsPerServer { get; set; } + + /// + /// + /// + public int? MaxResponseHeadersLength { get; set; } + + /// + /// + /// + public bool? PreAuthenticate { get; set; } + + /// + /// + /// + public RequestCachePolicy RequestCachePolicy { get; set; } + + /// + /// + /// + public string Scheme { get; set; } = "https"; + + + /// + /// + /// + public TimeSpan? Timeout { get; set; } = TimeSpan.FromSeconds(30); + + /// + /// + /// + public string UserAgent { get; set; } = "RedmineDotNetAPIClient"; + + /// + /// + /// + public bool? UseCookies { get; set; } + +#if NETFRAMEWORK + /// + /// + /// + public bool CheckCertificateRevocationList { get; set; } + + /// + /// + /// + public long? MaxRequestContentBufferSize { get; set; } + + /// + /// + /// + public bool? UseDefaultCredentials { get; set; } +#endif + /// + /// + /// + public bool? UseProxy { get; set; } + + /// + /// + /// + /// Only HTTP/1.0 and HTTP/1.1 version requests are currently supported. + public Version ProtocolVersion { get; set; } + + + + +#if NET40_OR_GREATER || NETCOREAPP + /// + /// + /// + public X509CertificateCollection ClientCertificates { get; set; } +#endif + + +} \ No newline at end of file diff --git a/src/redmine-net-api/Http/RequestOptions.cs b/src/redmine-net-api/Http/RequestOptions.cs new file mode 100644 index 00000000..1e06ae53 --- /dev/null +++ b/src/redmine-net-api/Http/RequestOptions.cs @@ -0,0 +1,93 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System.Collections.Generic; +using System.Collections.Specialized; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.Http; + +/// +/// +/// +public sealed class RequestOptions +{ + /// + /// + /// + public NameValueCollection QueryString { get; set; } + /// + /// + /// + public string ImpersonateUser { get; set; } + /// + /// + /// + public string ContentType { get; set; } + /// + /// + /// + public string Accept { get; set; } + /// + /// + /// + public string UserAgent { get; set; } + + /// + /// + /// + public Dictionary Headers { get; set; } + + /// + /// + /// + /// + public RequestOptions Clone() + { + return new RequestOptions + { + QueryString = QueryString != null ? new NameValueCollection(QueryString) : null, + ImpersonateUser = ImpersonateUser, + ContentType = ContentType, + Accept = Accept, + UserAgent = UserAgent, + Headers = Headers != null ? new Dictionary(Headers) : null, + }; + } + + /// + /// + /// + /// + /// + public static RequestOptions Include(string include) + { + if (include.IsNullOrWhiteSpace()) + { + return null; + } + + var requestOptions = new RequestOptions + { + QueryString = new NameValueCollection + { + {RedmineKeys.INCLUDE, include} + } + }; + + return requestOptions; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/ICloneableOfT.cs b/src/redmine-net-api/ICloneableOfT.cs new file mode 100644 index 00000000..dd58fde2 --- /dev/null +++ b/src/redmine-net-api/ICloneableOfT.cs @@ -0,0 +1,14 @@ +namespace Redmine.Net.Api; + +/// +/// +/// +/// +public interface ICloneable +{ + /// + /// + /// + /// + internal T Clone(bool resetId); +} \ No newline at end of file diff --git a/src/redmine-net-api/IRedmineManager.Async.cs b/src/redmine-net-api/IRedmineManager.Async.cs new file mode 100644 index 00000000..0cb733b5 --- /dev/null +++ b/src/redmine-net-api/IRedmineManager.Async.cs @@ -0,0 +1,159 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +#if !(NET20) +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Redmine.Net.Api.Common; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api +{ + /// + /// + /// + public interface IRedmineManagerAsync + { + /// + /// Returns the count of items asynchronously for a given type T. + /// + /// The type of the results. + /// Optional request options. + /// Optional cancellation token. + /// The count of items as an integer. + Task CountAsync(RequestOptions requestOptions, CancellationToken cancellationToken = default) + where T : class, new(); + + /// + /// Gets the paginated objects asynchronous. + /// + /// The type of the results. + /// Optional request options. + /// Optional cancellation token. + /// A task representing the asynchronous operation that returns the paged results. + Task> GetPagedAsync(RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new(); + + /// + /// Gets the objects asynchronous. + /// + /// + /// + /// + /// + Task> GetAsync(RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new(); + + /// + /// Gets a Redmine object asynchronous. + /// + /// The type of object to retrieve. + /// The ID of the object to retrieve. + /// Optional request options. + /// Optional cancellation token. + /// The retrieved object of type T. + Task GetAsync(string id, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new(); + + /// + /// Creates a new Redmine object asynchronous. + /// + /// The type of the entity. + /// The entity to create. + /// The optional request options. + /// The cancellation token. + /// A Task representing the asynchronous operation, returning the created entity. + /// + /// This method creates an entity of type T asynchronously. It accepts an entity object, along with optional request options and cancellation token. + /// The method is generic and constrained to accept only classes that have a default constructor. + /// It uses the CreateAsync method to create the entity, passing the entity, request options, and cancellation token as arguments. + /// The method is awaited and returns a Task of type T representing the asynchronous operation. + /// + Task CreateAsync(T entity, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new(); + + /// + /// Creates a new Redmine object. This method does not block the calling thread. + /// + /// The type of the entity. + /// The entity object to create. + /// The ID of the owner. + /// Optional request options. + /// Optional cancellation token. + /// The created entity. + /// Thrown when an error occurs during the creation process. + Task CreateAsync(T entity, string ownerId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new(); + + /// + /// Updates the object asynchronous. + /// + /// The type of the entity. + /// The ID of the entity to update. + /// The entity to update. + /// Optional request options. + /// Optional cancellation token. + /// A task representing the asynchronous update operation. + /// + /// This method sends an update request to the Redmine API to update the entity with the specified ID. + /// + Task UpdateAsync(string id, T entity, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new(); + + /// + /// Deletes the Redmine object asynchronous. + /// + /// The type of the resource to delete. + /// The ID of the resource to delete. + /// Optional request options. + /// Cancellation token. + /// A task representing the asynchronous delete operation. + /// + /// This method sends a DELETE request to the Redmine API to delete a resource identified by the given ID. + /// + Task DeleteAsync(string id, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new(); + + /// + /// Support for adding attachments through the REST API is added in Redmine 1.4.0. + /// Upload a file to server. This method does not block the calling thread. + /// + /// The content of the file that will be uploaded on server. + /// + /// + /// + /// + /// . + /// + Task UploadFileAsync(byte[] data, string fileName = null, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + + /// + /// Downloads the file asynchronous. + /// + /// The address. + /// + /// + /// + /// + Task DownloadFileAsync(string address, RequestOptions requestOptions = null, IProgress progress = null, CancellationToken cancellationToken = default); + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api/IRedmineManager.cs b/src/redmine-net-api/IRedmineManager.cs new file mode 100644 index 00000000..e4830a43 --- /dev/null +++ b/src/redmine-net-api/IRedmineManager.cs @@ -0,0 +1,119 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections.Generic; +using Redmine.Net.Api.Common; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api; + +/// +/// +/// +public interface IRedmineManager +{ + /// + /// + /// + /// + /// + /// + int Count(RequestOptions requestOptions = null) + where T : class, new(); + + /// + /// + /// + /// + /// + /// + /// + T Get(string id, RequestOptions requestOptions = null) + where T : class, new(); + + /// + /// + /// + /// + /// + /// + List Get(RequestOptions requestOptions = null) + where T : class, new(); + + /// + /// + /// + /// + /// + /// + PagedResults GetPaginated(RequestOptions requestOptions = null) + where T : class, new(); + + /// + /// + /// + /// + /// + /// + /// + /// + T Create(T entity, string ownerId = null,RequestOptions requestOptions = null) + where T : class, new(); + + /// + /// + /// + /// + /// + /// + /// + /// + void Update(string id, T entity, string projectId = null, RequestOptions requestOptions = null) + where T : class, new(); + + /// + /// + /// + /// + /// + /// + void Delete(string id, RequestOptions requestOptions = null) + where T : class, new(); + + /// + /// Support for adding attachments through the REST API is added in Redmine 1.4.0. + /// Upload a file to the server. + /// + /// The content of the file that will be uploaded on server. + /// + /// + /// Returns the token for the uploaded file. + /// + /// + Upload UploadFile(byte[] data, string fileName = null); + + /// + /// Downloads a file from the specified address. + /// + /// The address. + /// + /// The content of the downloaded file as a byte array. + /// + byte[] DownloadFile(string address, IProgress progress = null); +} \ No newline at end of file diff --git a/src/redmine-net-api/Internals/ArgumentNullThrowHelper.cs b/src/redmine-net-api/Internals/ArgumentNullThrowHelper.cs new file mode 100644 index 00000000..e7daa84e --- /dev/null +++ b/src/redmine-net-api/Internals/ArgumentNullThrowHelper.cs @@ -0,0 +1,34 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +#nullable enable + +namespace Redmine.Net.Api.Internals; + +internal static class ArgumentNullThrowHelper +{ + public static void ThrowIfNull( + #if INTERNAL_NULLABLE_ATTRIBUTES || NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER + [NotNull] + #endif + object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) + { + #if !NET7_0_OR_GREATER || NETSTANDARD || NETFRAMEWORK + if (argument is null) + { + Throw(paramName); + } + #else + ArgumentNullException.ThrowIfNull(argument, paramName); + #endif + } + + #if !NET7_0_OR_GREATER || NETSTANDARD || NETFRAMEWORK + #if INTERNAL_NULLABLE_ATTRIBUTES || NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER + [DoesNotReturn] + #endif + internal static void Throw(string? paramName) => + throw new ArgumentNullException(paramName); + #endif +} \ No newline at end of file diff --git a/redmine-net20-api/Internals/HashCodeHelper.cs b/src/redmine-net-api/Internals/HashCodeHelper.cs old mode 100644 new mode 100755 similarity index 55% rename from redmine-net20-api/Internals/HashCodeHelper.cs rename to src/redmine-net-api/Internals/HashCodeHelper.cs index 1cde5a51..19f92d6a --- a/redmine-net20-api/Internals/HashCodeHelper.cs +++ b/src/redmine-net-api/Internals/HashCodeHelper.cs @@ -1,5 +1,5 @@ ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ You may obtain a copy of the License at limitations under the License. */ +using System; using System.Collections.Generic; namespace Redmine.Net.Api.Internals @@ -32,18 +33,49 @@ internal static class HashCodeHelper /// /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. /// - public static int GetHashCode(IList list, int hash) + public static int GetHashCode(IList list, int hash) where T : class { unchecked { var hashCode = hash; - if (list != null) + if (list == null) { - hashCode = (hashCode * 13) + list.Count; - foreach (T t in list) + return hashCode; + } + + hashCode = (hashCode * 17) + list.Count; + + foreach (var t in list) + { + hashCode *= 17; + if (t != null) + { + hashCode += t.GetHashCode(); + } + } + + return hashCode; + } + } + + public static int GetHashCode(List list, int hash) where T : class + { + unchecked + { + var hashCode = hash; + if (list == null) + { + return hashCode; + } + + hashCode = (hashCode * 17) + list.Count; + + foreach (var t in list) + { + hashCode *= 17; + if (t != null) { - hashCode *= 13; - if (t != null) hashCode = hashCode + t.GetHashCode(); + hashCode += t.GetHashCode(); } } @@ -66,7 +98,22 @@ public static int GetHashCode(T entity, int hash) { var hashCode = hash; - hashCode = (hashCode * 397) ^ (entity == null ? 0 : entity.GetHashCode()); + var type = typeof(T); + + var isNullable = Nullable.GetUnderlyingType(type) != null; + if (isNullable) + { + type = type.UnderlyingSystemType; + } + + if (type.IsValueType) + { + hashCode = (hashCode * 397) ^ entity.GetHashCode(); + } + else + { + hashCode = (hashCode * 397) ^ (entity?.GetHashCode() ?? 0); + } return hashCode; } diff --git a/src/redmine-net-api/Internals/HostHelper.cs b/src/redmine-net-api/Internals/HostHelper.cs new file mode 100644 index 00000000..e5a0fe0e --- /dev/null +++ b/src/redmine-net-api/Internals/HostHelper.cs @@ -0,0 +1,172 @@ +using System; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.Internals; + +internal static class HostHelper +{ + private static readonly char[] DotCharArray = ['.']; + + internal static void EnsureDomainNameIsValid(string domainName) + { + if (domainName.IsNullOrWhiteSpace()) + { + throw new RedmineException("Domain name cannot be null or empty."); + } + + if (domainName.Length > 255) + { + throw new RedmineException("Domain name cannot be longer than 255 characters."); + } + + var labels = domainName.Split(DotCharArray); + if (labels.Length == 1) + { + throw new RedmineException("Domain name is not valid."); + } + + foreach (var label in labels) + { + if (label.IsNullOrWhiteSpace() || label.Length > 63) + { + throw new RedmineException("Domain name must be between 1 and 63 characters."); + } + + if (!char.IsLetterOrDigit(label[0]) || !char.IsLetterOrDigit(label[label.Length - 1])) + { + throw new RedmineException("Domain name label starts or ends with a hyphen or invalid character."); + } + + for (var index = 0; index < label.Length; index++) + { + var ch = label[index]; + + if (!char.IsLetterOrDigit(ch) && ch != '-') + { + throw new RedmineException("Domain name contains an invalid character."); + } + + if (ch == '-' && index + 1 < label.Length && label[index + 1] == '-') + { + throw new RedmineException("Domain name contains consecutive hyphens."); + } + } + } + } + + internal static Uri CreateRedmineUri(string host, string scheme = null) + { + if (host.IsNullOrWhiteSpace()) + { + throw new RedmineException("The host is null or empty."); + } + + if (!Uri.TryCreate(host, UriKind.Absolute, out var uri)) + { + host = host.TrimEnd('/', '\\'); + EnsureDomainNameIsValid(host); + + if (!host.StartsWith(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) || + !host.StartsWith(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) + { + host = $"{scheme ?? Uri.UriSchemeHttps}://{host}"; + + if (!Uri.TryCreate(host, UriKind.Absolute, out uri)) + { + throw new RedmineException("The host is not valid."); + } + } + } + + if (!uri.IsWellFormedOriginalString()) + { + throw new RedmineException("The host is not well-formed."); + } + + scheme ??= Uri.UriSchemeHttps; + var hasScheme = false; + if (!uri.Scheme.IsNullOrWhiteSpace()) + { + if (uri.Host.IsNullOrWhiteSpace() && uri.IsAbsoluteUri && !uri.IsFile) + { + if (uri.Scheme.Equals("localhost", StringComparison.OrdinalIgnoreCase)) + { + int port = 0; + var portAsString = uri.AbsolutePath.RemoveTrailingSlash(); + if (!portAsString.IsNullOrWhiteSpace()) + { + int.TryParse(portAsString, out port); + } + + var ub = new UriBuilder(scheme, "localhost", port); + return ub.Uri; + } + } + else + { + if (!IsSchemaHttpOrHttps(uri.Scheme)) + { + throw new RedmineException("Invalid host scheme. Only HTTP and HTTPS are supported."); + } + + hasScheme = true; + } + } + else + { + if (!IsSchemaHttpOrHttps(scheme)) + { + throw new RedmineException("Invalid host scheme. Only HTTP and HTTPS are supported."); + } + } + + var uriBuilder = new UriBuilder(); + + if (uri.HostNameType == UriHostNameType.IPv6) + { + uriBuilder.Scheme = (hasScheme ? uri.Scheme : scheme ?? Uri.UriSchemeHttps); + uriBuilder.Host = uri.Host; + } + else + { + if (uri.Authority.IsNullOrWhiteSpace()) + { + if (uri.Port == -1) + { + if (int.TryParse(uri.LocalPath, out var port)) + { + uriBuilder.Port = port; + } + } + + uriBuilder.Scheme = scheme ?? Uri.UriSchemeHttps; + uriBuilder.Host = uri.Scheme; + } + else + { + uriBuilder.Scheme = uri.Scheme; + uriBuilder.Port = int.TryParse(uri.LocalPath, out var port) ? port : uri.Port; + uriBuilder.Host = uri.Host; + if (!uri.LocalPath.IsNullOrWhiteSpace() && !uri.LocalPath.Contains(".")) + { + uriBuilder.Path = uri.LocalPath; + } + } + } + + try + { + return uriBuilder.Uri; + } + catch (Exception ex) + { + throw new RedmineException($"Failed to create Redmine URI: {ex.Message}", ex); + } + } + + private static bool IsSchemaHttpOrHttps(string scheme) + { + return scheme == Uri.UriSchemeHttp || scheme == Uri.UriSchemeHttps; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Internals/ParameterValidator.cs b/src/redmine-net-api/Internals/ParameterValidator.cs new file mode 100644 index 00000000..55a4f944 --- /dev/null +++ b/src/redmine-net-api/Internals/ParameterValidator.cs @@ -0,0 +1,34 @@ +using System; + +namespace Redmine.Net.Api.Internals; + +/// +/// +/// +internal static class ParameterValidator +{ + public static void ValidateNotNull(T parameter, string parameterName) + where T : class + { + if (parameter is null) + { + throw new ArgumentNullException(parameterName); + } + } + + public static void ValidateNotNullOrEmpty(string parameter, string parameterName) + { + if (string.IsNullOrEmpty(parameter)) + { + throw new ArgumentException("Value cannot be null or empty", parameterName); + } + } + + public static void ValidateId(int id, string parameterName) + { + if (id <= 0) + { + throw new ArgumentException("Id must be greater than 0", parameterName); + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Logging/IRedmineLogger.cs b/src/redmine-net-api/Logging/IRedmineLogger.cs new file mode 100644 index 00000000..47b4c980 --- /dev/null +++ b/src/redmine-net-api/Logging/IRedmineLogger.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + +namespace Redmine.Net.Api.Logging; + +/// +/// Provides abstraction for logging operations +/// +public interface IRedmineLogger +{ + /// + /// Checks if the specified log level is enabled + /// + bool IsEnabled(LogLevel level); + + /// + /// Logs a message with the specified level + /// + void Log(LogLevel level, string message, Exception exception = null); + + /// + /// Creates a scoped logger with additional context + /// + IRedmineLogger CreateScope(string scopeName, IDictionary scopeProperties = null); +} \ No newline at end of file diff --git a/src/redmine-net-api/Logging/LogLevel.cs b/src/redmine-net-api/Logging/LogLevel.cs new file mode 100644 index 00000000..a58e1500 --- /dev/null +++ b/src/redmine-net-api/Logging/LogLevel.cs @@ -0,0 +1,32 @@ +namespace Redmine.Net.Api.Logging; + +/// +/// Defines logging severity levels +/// +public enum LogLevel +{ + /// + /// + /// + Trace, + /// + /// + /// + Debug, + /// + /// + /// + Information, + /// + /// + /// + Warning, + /// + /// + /// + Error, + /// + /// + /// + Critical +} \ No newline at end of file diff --git a/src/redmine-net-api/Logging/MicrosoftLoggerRedmineAdapter.cs b/src/redmine-net-api/Logging/MicrosoftLoggerRedmineAdapter.cs new file mode 100644 index 00000000..1ac86ef6 --- /dev/null +++ b/src/redmine-net-api/Logging/MicrosoftLoggerRedmineAdapter.cs @@ -0,0 +1,92 @@ +#if NET462_OR_GREATER || NETCOREAPP +using System; +using System.Collections.Generic; + +namespace Redmine.Net.Api.Logging; + +/// +/// Adapter that converts Microsoft.Extensions.Logging.ILogger to IRedmineLogger +/// +public class MicrosoftLoggerRedmineAdapter : IRedmineLogger +{ + private readonly Microsoft.Extensions.Logging.ILogger _microsoftLogger; + + /// + /// Creates a new adapter for Microsoft.Extensions.Logging.ILogger + /// + /// The Microsoft logger to adapt + /// Thrown if microsoftLogger is null + public MicrosoftLoggerRedmineAdapter(Microsoft.Extensions.Logging.ILogger microsoftLogger) + { + _microsoftLogger = microsoftLogger ?? throw new ArgumentNullException(nameof(microsoftLogger)); + } + + /// + /// Checks if logging is enabled for the specified level + /// + public bool IsEnabled(LogLevel level) + { + return _microsoftLogger.IsEnabled(ToMicrosoftLogLevel(level)); + } + + /// + /// Logs a message with the specified level + /// + public void Log(LogLevel level, string message, Exception exception = null) + { + _microsoftLogger.Log( + ToMicrosoftLogLevel(level), + 0, // eventId + message, + exception, + (s, e) => s); + } + + /// + /// Creates a scoped logger with additional context + /// + public IRedmineLogger CreateScope(string scopeName, IDictionary scopeProperties = null) + { + var scopeData = new Dictionary + { + ["ScopeName"] = scopeName + }; + + // Add additional properties if provided + if (scopeProperties != null) + { + foreach (var prop in scopeProperties) + { + scopeData[prop.Key] = prop.Value; + } + } + + // Create a single scope with all properties + var disposableScope = _microsoftLogger.BeginScope(scopeData); + + // Return a new adapter that will close the scope when disposed + return new ScopedMicrosoftLoggerAdapter(_microsoftLogger, disposableScope); + } + + private class ScopedMicrosoftLoggerAdapter(Microsoft.Extensions.Logging.ILogger logger, IDisposable scope) + : MicrosoftLoggerRedmineAdapter(logger), IDisposable + { + public void Dispose() + { + scope?.Dispose(); + } + } + + + private static Microsoft.Extensions.Logging.LogLevel ToMicrosoftLogLevel(LogLevel level) => level switch + { + LogLevel.Trace => Microsoft.Extensions.Logging.LogLevel.Trace, + LogLevel.Debug => Microsoft.Extensions.Logging.LogLevel.Debug, + LogLevel.Information => Microsoft.Extensions.Logging.LogLevel.Information, + LogLevel.Warning => Microsoft.Extensions.Logging.LogLevel.Warning, + LogLevel.Error => Microsoft.Extensions.Logging.LogLevel.Error, + LogLevel.Critical => Microsoft.Extensions.Logging.LogLevel.Critical, + _ => Microsoft.Extensions.Logging.LogLevel.Information + }; +} +#endif diff --git a/src/redmine-net-api/Logging/RedmineConsoleLogger.cs b/src/redmine-net-api/Logging/RedmineConsoleLogger.cs new file mode 100644 index 00000000..23b03cea --- /dev/null +++ b/src/redmine-net-api/Logging/RedmineConsoleLogger.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; + +namespace Redmine.Net.Api.Logging; + +/// +/// +/// +/// +/// +/// +/// +/// +public class RedmineConsoleLogger(string categoryName = "Redmine", LogLevel minLevel = LogLevel.Information) : IRedmineLogger +{ + /// + /// + /// + /// + /// + public bool IsEnabled(LogLevel level) => level >= minLevel; + + /// + /// + /// + /// + /// + /// + public void Log(LogLevel level, string message, Exception exception = null) + { + if (!IsEnabled(level)) + { + return; + } + + // var originalColor = Console.ForegroundColor; + // + // Console.ForegroundColor = level switch + // { + // LogLevel.Trace => ConsoleColor.Gray, + // LogLevel.Debug => ConsoleColor.Gray, + // LogLevel.Information => ConsoleColor.White, + // LogLevel.Warning => ConsoleColor.Yellow, + // LogLevel.Error => ConsoleColor.Red, + // LogLevel.Critical => ConsoleColor.Red, + // _ => ConsoleColor.White + // }; + + Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [{level}] [{categoryName}] {message}"); + + if (exception != null) + { + Console.WriteLine($"Exception: {exception}"); + } + + // Console.ForegroundColor = originalColor; + } + + /// + /// + /// + /// + /// + /// + public IRedmineLogger CreateScope(string scopeName, IDictionary scopeProperties = null) + { + return new RedmineConsoleLogger($"{categoryName}.{scopeName}", minLevel); + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Logging/RedmineLoggerExtensions.cs b/src/redmine-net-api/Logging/RedmineLoggerExtensions.cs new file mode 100644 index 00000000..39012412 --- /dev/null +++ b/src/redmine-net-api/Logging/RedmineLoggerExtensions.cs @@ -0,0 +1,108 @@ +using System; +using System.Diagnostics; +#if !(NET20 || NET40) +using System.Threading.Tasks; +#endif +namespace Redmine.Net.Api.Logging; + +/// +/// +/// +public static class RedmineLoggerExtensions +{ + /// + /// + /// + /// + /// + /// + public static void Trace(this IRedmineLogger logger, string message, Exception exception = null) + => logger.Log(LogLevel.Trace, message, exception); + + /// + /// + /// + /// + /// + /// + public static void Debug(this IRedmineLogger logger, string message, Exception exception = null) + => logger.Log(LogLevel.Debug, message, exception); + + /// + /// + /// + /// + /// + /// + public static void Info(this IRedmineLogger logger, string message, Exception exception = null) + => logger.Log(LogLevel.Information, message, exception); + + /// + /// + /// + /// + /// + /// + public static void Warn(this IRedmineLogger logger, string message, Exception exception = null) + => logger.Log(LogLevel.Warning, message, exception); + + /// + /// + /// + /// + /// + /// + public static void Error(this IRedmineLogger logger, string message, Exception exception = null) + => logger.Log(LogLevel.Error, message, exception); + + /// + /// + /// + /// + /// + /// + public static void Critical(this IRedmineLogger logger, string message, Exception exception = null) + => logger.Log(LogLevel.Critical, message, exception); + +#if !(NET20 || NET40) + /// + /// Creates and logs timing information for an operation + /// + public static async Task TimeOperationAsync(this IRedmineLogger logger, string operationName, Func> operation) + { + if (!logger.IsEnabled(LogLevel.Debug)) + return await operation().ConfigureAwait(false); + + var sw = Stopwatch.StartNew(); + try + { + return await operation().ConfigureAwait(false); + } + finally + { + sw.Stop(); + logger.Debug($"Operation '{operationName}' completed in {sw.ElapsedMilliseconds}ms"); + } + } + #endif + + /// + /// Creates and logs timing information for an operation + /// + public static T TimeOperationAsync(this IRedmineLogger logger, string operationName, Func operation) + { + if (!logger.IsEnabled(LogLevel.Debug)) + return operation(); + + var sw = Stopwatch.StartNew(); + try + { + return operation(); + } + finally + { + sw.Stop(); + logger.Debug($"Operation '{operationName}' completed in {sw.ElapsedMilliseconds}ms"); + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Logging/RedmineLoggerFactory.cs b/src/redmine-net-api/Logging/RedmineLoggerFactory.cs new file mode 100644 index 00000000..9b2dddff --- /dev/null +++ b/src/redmine-net-api/Logging/RedmineLoggerFactory.cs @@ -0,0 +1,75 @@ +#if NET462_OR_GREATER || NETCOREAPP +namespace Redmine.Net.Api.Logging; + +/// +/// +/// +public static class RedmineLoggerFactory +{ + /// + /// + /// + /// + /// + /// + public static Microsoft.Extensions.Logging.ILogger CreateMicrosoftLoggerAdapter(IRedmineLogger redmineLogger, + string categoryName = "Redmine") + { + if (redmineLogger == null || redmineLogger == RedmineNullLogger.Instance) + { + return Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance; + } + + return new RedmineLoggerMicrosoftAdapter(redmineLogger, categoryName); + } + + /// + /// Creates an adapter that exposes a Microsoft.Extensions.Logging.ILogger as IRedmineLogger + /// + /// The Microsoft logger to adapt + /// A Redmine logger implementation + public static IRedmineLogger CreateMicrosoftLogger(Microsoft.Extensions.Logging.ILogger microsoftLogger) + { + return microsoftLogger != null + ? new MicrosoftLoggerRedmineAdapter(microsoftLogger) + : RedmineNullLogger.Instance; + } + + /// + /// Creates a logger that writes to the console + /// + public static IRedmineLogger CreateConsoleLogger(LogLevel minLevel = LogLevel.Information) + { + return new RedmineConsoleLogger(minLevel: minLevel); + } + + // /// + // /// Creates an adapter for Serilog + // /// + // public static IRedmineLogger CreateSerilogAdapter(Serilog.ILogger logger) + // { + // if (logger == null) return NullRedmineLogger.Instance; + // return new SerilogAdapter(logger); + // } + // + // /// + // /// Creates an adapter for NLog + // /// + // public static IRedmineLogger CreateNLogAdapter(NLog.ILogger logger) + // { + // if (logger == null) return NullRedmineLogger.Instance; + // return new NLogAdapter(logger); + // } + // + // /// + // /// Creates an adapter for log4net + // /// + // public static IRedmineLogger CreateLog4NetAdapter(log4net.ILog logger) + // { + // if (logger == null) return NullRedmineLogger.Instance; + // return new Log4NetAdapter(logger); + // } +} + + +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Logging/RedmineLoggerMicrosoftAdapter.cs b/src/redmine-net-api/Logging/RedmineLoggerMicrosoftAdapter.cs new file mode 100644 index 00000000..56d152f9 --- /dev/null +++ b/src/redmine-net-api/Logging/RedmineLoggerMicrosoftAdapter.cs @@ -0,0 +1,97 @@ +#if NET462_OR_GREATER || NETCOREAPP +using System; +using System.Collections.Generic; + +namespace Redmine.Net.Api.Logging; + +/// +/// +/// +public class RedmineLoggerMicrosoftAdapter : Microsoft.Extensions.Logging.ILogger +{ + private readonly IRedmineLogger _redmineLogger; + private readonly string _categoryName; + + /// + /// + /// + /// + /// + /// + public RedmineLoggerMicrosoftAdapter(IRedmineLogger redmineLogger, string categoryName = "Redmine.Net.Api") + { + _redmineLogger = redmineLogger ?? throw new ArgumentNullException(nameof(redmineLogger)); + _categoryName = categoryName; + } + + /// + /// + /// + /// + /// + /// + public IDisposable BeginScope(TState state) + { + if (state is IDictionary dict) + { + _redmineLogger.CreateScope("Scope", dict); + } + else + { + var scopeName = state?.ToString() ?? "Scope"; + _redmineLogger.CreateScope(scopeName); + } + + return new NoOpDisposable(); + } + + /// + /// + /// + /// + /// + public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel) + { + return _redmineLogger.IsEnabled(ToRedmineLogLevel(logLevel)); + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public void Log( + Microsoft.Extensions.Logging.LogLevel logLevel, + Microsoft.Extensions.Logging.EventId eventId, + TState state, + Exception exception, + Func formatter) + { + if (!IsEnabled(logLevel)) + return; + + var message = formatter(state, exception); + _redmineLogger.Log(ToRedmineLogLevel(logLevel), message, exception); + } + + private static LogLevel ToRedmineLogLevel(Microsoft.Extensions.Logging.LogLevel level) => level switch + { + Microsoft.Extensions.Logging.LogLevel.Trace => LogLevel.Trace, + Microsoft.Extensions.Logging.LogLevel.Debug => LogLevel.Debug, + Microsoft.Extensions.Logging.LogLevel.Information => LogLevel.Information, + Microsoft.Extensions.Logging.LogLevel.Warning => LogLevel.Warning, + Microsoft.Extensions.Logging.LogLevel.Error => LogLevel.Error, + Microsoft.Extensions.Logging.LogLevel.Critical => LogLevel.Critical, + _ => LogLevel.Information + }; + + private class NoOpDisposable : IDisposable + { + public void Dispose() { } + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Logging/RedmineLoggingOptions.cs b/src/redmine-net-api/Logging/RedmineLoggingOptions.cs new file mode 100644 index 00000000..d7b510d4 --- /dev/null +++ b/src/redmine-net-api/Logging/RedmineLoggingOptions.cs @@ -0,0 +1,22 @@ +namespace Redmine.Net.Api.Logging; + +/// +/// Options for configuring Redmine logging +/// +public sealed class RedmineLoggingOptions +{ + /// + /// Gets or sets the minimum log level. The default value is LogLevel.Information + /// + public LogLevel MinimumLevel { get; set; } = LogLevel.Information; + + /// + /// Gets or sets whether to include HTTP request/response details in logs + /// + public bool IncludeHttpDetails { get; set; } + + /// + /// Gets or sets whether performance metrics should be logged + /// + public bool LogPerformanceMetrics { get; set; } +} \ No newline at end of file diff --git a/src/redmine-net-api/Logging/RedmineNullLogger.cs b/src/redmine-net-api/Logging/RedmineNullLogger.cs new file mode 100644 index 00000000..0d47ab1d --- /dev/null +++ b/src/redmine-net-api/Logging/RedmineNullLogger.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; + +namespace Redmine.Net.Api.Logging; + +/// +/// +/// +public class RedmineNullLogger : IRedmineLogger +{ + /// + /// + /// + public static readonly RedmineNullLogger Instance = new RedmineNullLogger(); + + private RedmineNullLogger() { } + + /// + /// + /// + /// + /// + public bool IsEnabled(LogLevel level) => false; + + /// + /// + /// + /// + /// + /// + public void Log(LogLevel level, string message, Exception exception = null) { } + + /// + /// + /// + /// + /// + /// + public IRedmineLogger CreateScope(string scopeName, IDictionary scopeProperties = null) => this; +} \ No newline at end of file diff --git a/src/redmine-net-api/Net/Internal/RedmineApiUrls.cs b/src/redmine-net-api/Net/Internal/RedmineApiUrls.cs new file mode 100644 index 00000000..8fa0b3a1 --- /dev/null +++ b/src/redmine-net-api/Net/Internal/RedmineApiUrls.cs @@ -0,0 +1,268 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections.Generic; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Types; +using Version = Redmine.Net.Api.Types.Version; + +namespace Redmine.Net.Api.Net.Internal +{ + internal sealed class RedmineApiUrls + { + public string Format { get; init; } + + private static readonly Dictionary TypeUrlFragments = new Dictionary() + { + {typeof(Attachment), RedmineKeys.ATTACHMENTS}, + {typeof(CustomField), RedmineKeys.CUSTOM_FIELDS}, + {typeof(DocumentCategory), RedmineKeys.ENUMERATION_DOCUMENT_CATEGORIES}, + {typeof(Group), RedmineKeys.GROUPS}, + {typeof(Issue), RedmineKeys.ISSUES}, + {typeof(IssueCategory), RedmineKeys.ISSUE_CATEGORIES}, + {typeof(IssueCustomField), RedmineKeys.CUSTOM_FIELDS}, + {typeof(IssuePriority), RedmineKeys.ENUMERATION_ISSUE_PRIORITIES}, + {typeof(IssueRelation), RedmineKeys.RELATIONS}, + {typeof(IssueStatus), RedmineKeys.ISSUE_STATUSES}, + {typeof(Journal), RedmineKeys.JOURNALS}, + {typeof(News), RedmineKeys.NEWS}, + {typeof(Project), RedmineKeys.PROJECTS}, + {typeof(ProjectMembership), RedmineKeys.MEMBERSHIPS}, + {typeof(Query), RedmineKeys.QUERIES}, + {typeof(Role), RedmineKeys.ROLES}, + {typeof(Search), RedmineKeys.SEARCH}, + {typeof(TimeEntry), RedmineKeys.TIME_ENTRIES}, + {typeof(TimeEntryActivity), RedmineKeys.ENUMERATION_TIME_ENTRY_ACTIVITIES}, + {typeof(Tracker), RedmineKeys.TRACKERS}, + {typeof(User), RedmineKeys.USERS}, + {typeof(Version), RedmineKeys.VERSIONS}, + {typeof(Watcher), RedmineKeys.WATCHERS}, + }; + + public RedmineApiUrls(string format) + { + Format = format; + } + + public string ProjectFilesFragment(string projectId) + { + if (string.IsNullOrEmpty(projectId)) + { + throw new RedmineException("The owner id(project id) is mandatory!"); + } + + return $"{RedmineKeys.PROJECTS}/{projectId}/{RedmineKeys.FILES}.{Format}"; + } + + public string IssueAttachmentFragment(string issueId) + { + if (issueId.IsNullOrWhiteSpace()) + { + throw new RedmineException("The issue id is mandatory!"); + } + + return $"/{RedmineKeys.ATTACHMENTS}/{RedmineKeys.ISSUES}/{issueId}.{Format}"; + } + + public string ProjectParentFragment(string projectId, string mapTypeFragment) + { + if (string.IsNullOrEmpty(projectId)) + { + throw new RedmineException("The owner project id is mandatory!"); + } + + return $"{RedmineKeys.PROJECTS}/{projectId}/{mapTypeFragment}.{Format}"; + } + + public string IssueParentFragment(string issueId, string mapTypeFragment) + { + if (string.IsNullOrEmpty(issueId)) + { + throw new RedmineException("The owner issue id is mandatory!"); + } + + return $"{RedmineKeys.ISSUES}/{issueId}/{mapTypeFragment}.{Format}"; + } + + public static string TypeFragment(Dictionary mapTypeUrlFragments, Type type) + { + if (!mapTypeUrlFragments.TryGetValue(type, out var fragment)) + { + throw new RedmineException($"There is no uri fragment defined for type {type.Name}"); + } + + return fragment; + } + + public string CreateEntityFragment(string ownerId = null) + { + var type = typeof(T); + + return CreateEntityFragment(type, ownerId); + } + public string CreateEntityFragment(RequestOptions requestOptions) + { + var type = typeof(T); + + return CreateEntityFragment(type, requestOptions); + } + internal string CreateEntityFragment(Type type, RequestOptions requestOptions) + { + string ownerId = null; + if (requestOptions is { QueryString: not null }) + { + ownerId = requestOptions.QueryString.Get(RedmineKeys.PROJECT_ID) ?? + requestOptions.QueryString.Get(RedmineKeys.ISSUE_ID); + } + + return CreateEntityFragment(type, ownerId); + } + internal string CreateEntityFragment(Type type, string ownerId = null) + { + if (type == typeof(Version) || type == typeof(IssueCategory) || type == typeof(ProjectMembership)) + { + return ProjectParentFragment(ownerId, TypeUrlFragments[type]); + } + + if (type == typeof(IssueRelation)) + { + return IssueParentFragment(ownerId, TypeUrlFragments[type]); + } + + if (type == typeof(File)) + { + return ProjectFilesFragment(ownerId); + } + + if (type == typeof(Upload)) + { + return UploadFragment(ownerId); //$"{RedmineKeys.UPLOADS}.{Format}"; + } + + if (type == typeof(Attachment) || type == typeof(Attachments)) + { + return IssueAttachmentFragment(ownerId); + } + + return $"{TypeFragment(TypeUrlFragments, type)}.{Format}"; + } + + public string GetFragment(string id) where T : class, new() + { + var type = typeof(T); + + return GetFragment(type, id); + } + internal string GetFragment(Type type, string id) + { + return $"{TypeFragment(TypeUrlFragments, type)}/{id}.{Format}"; + } + + public string PatchFragment(string ownerId) + { + var type = typeof(T); + + return PatchFragment(type, ownerId); + } + internal string PatchFragment(Type type, string ownerId) + { + if (type == typeof(Attachment) || type == typeof(Attachments)) + { + return IssueAttachmentFragment(ownerId); + } + + throw new RedmineException($"No endpoint defined for type {type} for PATCH operation."); + } + + public string DeleteFragment(string id) + { + var type = typeof(T); + + return DeleteFragment(type, id); + } + internal string DeleteFragment(Type type, string id) + { + return $"{TypeFragment(TypeUrlFragments, type)}/{id}.{Format}"; + } + + public string UpdateFragment(string id) + { + var type = typeof(T); + + return UpdateFragment(type, id); + } + internal string UpdateFragment(Type type, string id) + { + return $"{TypeFragment(TypeUrlFragments, type)}/{id}.{Format}"; + } + + public string GetListFragment(string ownerId = null) where T : class, new() + { + var type = typeof(T); + + return GetListFragment(type, ownerId); + } + + public string GetListFragment(RequestOptions requestOptions) where T : class, new() + { + var type = typeof(T); + + return GetListFragment(type, requestOptions); + } + + internal string GetListFragment(Type type, RequestOptions requestOptions) + { + string ownerId = null; + if (requestOptions is { QueryString: not null }) + { + ownerId = requestOptions.QueryString.Get(RedmineKeys.PROJECT_ID) ?? + requestOptions.QueryString.Get(RedmineKeys.ISSUE_ID); + } + + return GetListFragment(type, ownerId); + } + + internal string GetListFragment(Type type, string ownerId = null) + { + if (type == typeof(Version) || type == typeof(IssueCategory) || type == typeof(ProjectMembership)) + { + return ProjectParentFragment(ownerId, TypeUrlFragments[type]); + } + + if (type == typeof(IssueRelation)) + { + return IssueParentFragment(ownerId, TypeUrlFragments[type]); + } + + if (type == typeof(File)) + { + return ProjectFilesFragment(ownerId); + } + + return $"{TypeFragment(TypeUrlFragments, type)}.{Format}"; + } + + public string UploadFragment(string fileName = null) + { + return !fileName.IsNullOrWhiteSpace() + ? $"{RedmineKeys.UPLOADS}.{Format}?filename={Uri.EscapeDataString(fileName)}" + : $"{RedmineKeys.UPLOADS}.{Format}"; + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Net/Internal/RedmineApiUrlsExtensions.cs b/src/redmine-net-api/Net/Internal/RedmineApiUrlsExtensions.cs new file mode 100644 index 00000000..753b439d --- /dev/null +++ b/src/redmine-net-api/Net/Internal/RedmineApiUrlsExtensions.cs @@ -0,0 +1,135 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +namespace Redmine.Net.Api.Net.Internal; + +internal static class RedmineApiUrlsExtensions +{ + public static string MyAccount(this RedmineApiUrls redmineApiUrls) + { + return $"{RedmineKeys.MY}/{RedmineKeys.ACCOUNT}.{redmineApiUrls.Format}"; + } + + public static string CurrentUser(this RedmineApiUrls redmineApiUrls) + { + return $"{RedmineKeys.USERS}/{RedmineKeys.CURRENT}.{redmineApiUrls.Format}"; + } + + public static string ProjectClose(this RedmineApiUrls redmineApiUrls, string projectIdentifier) + { + return $"{RedmineKeys.PROJECTS}/{projectIdentifier}/{RedmineKeys.CLOSE}.{redmineApiUrls.Format}"; + } + + public static string ProjectReopen(this RedmineApiUrls redmineApiUrls, string projectIdentifier) + { + return $"{RedmineKeys.PROJECTS}/{projectIdentifier}/{RedmineKeys.REOPEN}.{redmineApiUrls.Format}"; + } + + public static string ProjectArchive(this RedmineApiUrls redmineApiUrls, string projectIdentifier) + { + return $"{RedmineKeys.PROJECTS}/{projectIdentifier}/{RedmineKeys.ARCHIVE}.{redmineApiUrls.Format}"; + } + + public static string ProjectUnarchive(this RedmineApiUrls redmineApiUrls, string projectIdentifier) + { + return $"{RedmineKeys.PROJECTS}/{projectIdentifier}/{RedmineKeys.UNARCHIVE}.{redmineApiUrls.Format}"; + } + + public static string ProjectRepositoryAddRelatedIssue(this RedmineApiUrls redmineApiUrls, string projectIdentifier, string repositoryIdentifier, string revision) + { + return $"{RedmineKeys.PROJECTS}/{projectIdentifier}/{RedmineKeys.REPOSITORY}/{repositoryIdentifier}/{RedmineKeys.REVISIONS}/{revision}/{RedmineKeys.ISSUES}.{redmineApiUrls.Format}"; + } + + public static string ProjectRepositoryRemoveRelatedIssue(this RedmineApiUrls redmineApiUrls, string projectIdentifier, string repositoryIdentifier, string revision, string issueIdentifier) + { + return $"{RedmineKeys.PROJECTS}/{projectIdentifier}/{RedmineKeys.REPOSITORY}/{repositoryIdentifier}/{RedmineKeys.REVISIONS}/{revision}/{RedmineKeys.ISSUES}/{issueIdentifier}.{redmineApiUrls.Format}"; + } + + public static string ProjectNews(this RedmineApiUrls redmineApiUrls, string projectIdentifier) + { + return $"{RedmineKeys.PROJECTS}/{projectIdentifier}/{RedmineKeys.NEWS}.{redmineApiUrls.Format}"; + } + + public static string ProjectMemberships(this RedmineApiUrls redmineApiUrls, string projectIdentifier) + { + return $"{RedmineKeys.PROJECTS}/{projectIdentifier}/{RedmineKeys.MEMBERSHIPS}.{redmineApiUrls.Format}"; + } + + public static string ProjectWikiIndex(this RedmineApiUrls redmineApiUrls, string projectId) + { + return $"{RedmineKeys.PROJECTS}/{projectId}/{RedmineKeys.WIKI}/{RedmineKeys.INDEX}.{redmineApiUrls.Format}"; + } + + public static string ProjectWikiPage(this RedmineApiUrls redmineApiUrls, string projectId, string wikiPageName) + { + return $"{RedmineKeys.PROJECTS}/{projectId}/{RedmineKeys.WIKI}/{wikiPageName}.{redmineApiUrls.Format}"; + } + + public static string ProjectWikiPageVersion(this RedmineApiUrls redmineApiUrls, string projectId, string wikiPageName, string version) + { + return $"{RedmineKeys.PROJECTS}/{projectId}/{RedmineKeys.WIKI}/{wikiPageName}/{version}.{redmineApiUrls.Format}"; + } + + public static string ProjectWikiPageCreate(this RedmineApiUrls redmineApiUrls, string projectId, string wikiPageName) + { + return $"{RedmineKeys.PROJECTS}/{projectId}/{RedmineKeys.WIKI}/{wikiPageName}.{redmineApiUrls.Format}"; + } + + public static string ProjectWikiPageUpdate(this RedmineApiUrls redmineApiUrls, string projectId, string wikiPageName) + { + return $"{RedmineKeys.PROJECTS}/{projectId}/{RedmineKeys.WIKI}/{wikiPageName}.{redmineApiUrls.Format}"; + } + + public static string ProjectWikiPageDelete(this RedmineApiUrls redmineApiUrls, string projectId, string wikiPageName) + { + return $"{RedmineKeys.PROJECTS}/{projectId}/{RedmineKeys.WIKI}/{wikiPageName}.{redmineApiUrls.Format}"; + } + + public static string ProjectWikis(this RedmineApiUrls redmineApiUrls, string projectId) + { + return ProjectWikiIndex(redmineApiUrls, projectId); + } + + public static string IssueWatcherAdd(this RedmineApiUrls redmineApiUrls, string issueIdentifier) + { + return $"{RedmineKeys.ISSUES}/{issueIdentifier}/{RedmineKeys.WATCHERS}.{redmineApiUrls.Format}"; + } + + public static string IssueWatcherRemove(this RedmineApiUrls redmineApiUrls, string issueIdentifier, string userId) + { + return $"{RedmineKeys.ISSUES}/{issueIdentifier}/{RedmineKeys.WATCHERS}/{userId}.{redmineApiUrls.Format}"; + } + + public static string GroupUserAdd(this RedmineApiUrls redmineApiUrls, string groupIdentifier) + { + return $"{RedmineKeys.GROUPS}/{groupIdentifier}/{RedmineKeys.USERS}.{redmineApiUrls.Format}"; + } + + public static string GroupUserRemove(this RedmineApiUrls redmineApiUrls, string groupIdentifier, string userId) + { + return $"{RedmineKeys.GROUPS}/{groupIdentifier}/{RedmineKeys.USERS}/{userId}.{redmineApiUrls.Format}"; + } + + public static string AttachmentUpdate(this RedmineApiUrls redmineApiUrls, string issueIdentifier) + { + return $"{RedmineKeys.ATTACHMENTS}/{RedmineKeys.ISSUES}/{issueIdentifier}.{redmineApiUrls.Format}"; + } + + public static string Uploads(this RedmineApiUrls redmineApiUrls) + { + return $"{RedmineKeys.UPLOADS}.{redmineApiUrls.Format}"; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Options/RedmineManagerOptions.cs b/src/redmine-net-api/Options/RedmineManagerOptions.cs new file mode 100644 index 00000000..7093c073 --- /dev/null +++ b/src/redmine-net-api/Options/RedmineManagerOptions.cs @@ -0,0 +1,106 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Net; +using Redmine.Net.Api.Authentication; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Http.Clients.WebClient; +using Redmine.Net.Api.Logging; +using Redmine.Net.Api.Serialization; +#if !NET20 +using System.Net.Http; +using Redmine.Net.Api.Http.Clients.HttpClient; +#endif + +namespace Redmine.Net.Api.Options +{ + /// + /// + /// + internal sealed class RedmineManagerOptions + { + /// + /// + /// + public Uri BaseAddress { get; init; } + + /// + /// Gets or sets the page size for paginated Redmine API responses. + /// The default page size is 25, but you can customize it as needed. + /// + public int PageSize { get; init; } + + /// + /// Gets or sets the desired MIME format for Redmine API responses, which represents the way of serialization. + /// Supported formats include XML and JSON. The default format is XML. + /// + public IRedmineSerializer Serializer { get; init; } + + /// + /// Gets or sets the authentication method to be used when connecting to the Redmine server. + /// The available authentication types include API token-based authentication and basic authentication + /// (using a username and password). You can set an instance of the corresponding authentication class + /// to use the desired authentication method. + /// + public IRedmineAuthentication Authentication { get; init; } + + /// + /// Gets or sets the version of the Redmine server to which this client will connect. + /// + public Version RedmineVersion { get; init; } + + public IRedmineLogger Logger { get; init; } + + /// + /// Gets or sets additional logging configuration options + /// + public RedmineLoggingOptions LoggingOptions { get; init; } = new RedmineLoggingOptions(); + + /// + /// Gets or sets the settings for configuring the Redmine http client. + /// + public IRedmineApiClientOptions ApiClientOptions { get; set; } + + /// + /// Gets or sets a custom function that creates and returns a specialized instance of the WebClient class. + /// + public Func ClientFunc { get; init; } + + /// + /// Gets or sets the settings for configuring the Redmine web client. + /// + public RedmineWebClientOptions WebClientOptions { + get => (RedmineWebClientOptions)ApiClientOptions; + set => ApiClientOptions = value; + } + + #if !NET20 + /// + /// + /// + public HttpClient HttpClient { get; init; } + + /// + /// Gets or sets the settings for configuring the Redmine http client. + /// + public RedmineHttpClientOptions HttpClientOptions { + get => (RedmineHttpClientOptions)ApiClientOptions; + set => ApiClientOptions = value; + } + #endif + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Options/RedmineManagerOptionsBuilder.cs b/src/redmine-net-api/Options/RedmineManagerOptionsBuilder.cs new file mode 100644 index 00000000..698cfb31 --- /dev/null +++ b/src/redmine-net-api/Options/RedmineManagerOptionsBuilder.cs @@ -0,0 +1,306 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Net; +#if NET462_OR_GREATER || NET +using Microsoft.Extensions.Logging; +#endif +using Redmine.Net.Api.Authentication; +using Redmine.Net.Api.Http; +#if !NET20 +using Redmine.Net.Api.Http.Clients.HttpClient; +#endif +using Redmine.Net.Api.Http.Clients.WebClient; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Logging; +using Redmine.Net.Api.Serialization; +#if NET40_OR_GREATER || NET +using System.Net.Http; +#endif +#if NET462_OR_GREATER || NET +#endif + +namespace Redmine.Net.Api.Options +{ + /// + /// + /// + public sealed class RedmineManagerOptionsBuilder + { + private IRedmineLogger _redmineLogger = RedmineNullLogger.Instance; + private Action _configureLoggingOptions; + + private enum ClientType + { + WebClient, + HttpClient, + } + private ClientType _clientType = ClientType.HttpClient; + + /// + /// + /// + public string Host { get; private set; } + + /// + /// + /// + public int PageSize { get; private set; } + + /// + /// Gets the current serialization type + /// + public SerializationType SerializationType { get; private set; } + + /// + /// + /// + public IRedmineAuthentication Authentication { get; private set; } + + /// + /// + /// + public IRedmineApiClientOptions ClientOptions { get; private set; } + + /// + /// + /// + public Func ClientFunc { get; private set; } + + /// + /// Gets or sets the version of the Redmine server to which this client will connect. + /// + public Version Version { get; set; } + + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithPageSize(int pageSize) + { + PageSize = pageSize; + return this; + } + + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithHost(string baseAddress) + { + Host = baseAddress; + return this; + } + + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithSerializationType(SerializationType serializationType) + { + SerializationType = serializationType; + return this; + } + + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithXmlSerialization() + { + SerializationType = SerializationType.Xml; + return this; + } + + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithJsonSerialization() + { + SerializationType = SerializationType.Json; + return this; + } + + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithApiKeyAuthentication(string apiKey) + { + Authentication = new RedmineApiKeyAuthentication(apiKey); + return this; + } + + /// + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithBasicAuthentication(string login, string password) + { + Authentication = new RedmineBasicAuthentication(login, password); + return this; + } + + /// + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithLogger(IRedmineLogger logger, Action configure = null) + { + _redmineLogger = logger ?? RedmineNullLogger.Instance; + _configureLoggingOptions = configure; + return this; + } + + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithVersion(Version version) + { + Version = version; + return this; + } + + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithWebClient(Func clientFunc) + { + _clientType = ClientType.WebClient; + ClientFunc = clientFunc; + return this; + } + + /// + /// Configures the client to use WebClient with default settings + /// + /// This builder instance for method chaining + public RedmineManagerOptionsBuilder UseWebClient(RedmineWebClientOptions clientOptions = null) + { + _clientType = ClientType.WebClient; + ClientOptions = clientOptions; + return this; + } + +#if NET40_OR_GREATER || NET + /// + /// + /// + public Func HttpClientFunc { get; private set; } + + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithHttpClient(Func clientFunc) + { + _clientType = ClientType.HttpClient; + this.HttpClientFunc = clientFunc; + return this; + } + + /// + /// Configures the client to use HttpClient with default settings + /// + /// This builder instance for method chaining + public RedmineManagerOptionsBuilder UseHttpClient(RedmineHttpClientOptions clientOptions = null) + { + _clientType = ClientType.HttpClient; + ClientOptions = clientOptions; + return this; + } + +#endif + +#if NET462_OR_GREATER || NET + /// + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithLogger(ILogger logger, Action configure = null) + { + _redmineLogger = new MicrosoftLoggerRedmineAdapter(logger); + _configureLoggingOptions = configure; + return this; + } +#endif + + /// + /// + /// + /// + internal RedmineManagerOptions Build() + { +#if NET45_OR_GREATER || NET + ClientOptions ??= _clientType switch + { + ClientType.WebClient => new RedmineWebClientOptions(), + ClientType.HttpClient => new RedmineHttpClientOptions(), + _ => throw new ArgumentOutOfRangeException() + }; +#else + ClientOptions ??= new RedmineWebClientOptions(); +#endif + + var baseAddress = HostHelper.CreateRedmineUri(Host, ClientOptions.Scheme); + + var redmineLoggingOptions = ConfigureLoggingOptions(); + + var options = new RedmineManagerOptions() + { + BaseAddress = baseAddress, + PageSize = PageSize > 0 ? PageSize : RedmineConstants.DEFAULT_PAGE_SIZE_VALUE, + Serializer = RedmineSerializerFactory.CreateSerializer(SerializationType), + RedmineVersion = Version, + Authentication = Authentication ?? new RedmineNoAuthentication(), + ApiClientOptions = ClientOptions, + Logger = _redmineLogger, + LoggingOptions = redmineLoggingOptions, + }; + + return options; + } + + private RedmineLoggingOptions ConfigureLoggingOptions() + { + if (_configureLoggingOptions == null) + { + return null; + } + + var redmineLoggingOptions = new RedmineLoggingOptions(); + _configureLoggingOptions(redmineLoggingOptions); + return redmineLoggingOptions; + } + } +} \ No newline at end of file diff --git a/redmine-net20-api/Properties/AssemblyInfo.cs b/src/redmine-net-api/Properties/AssemblyInfo.cs similarity index 99% rename from redmine-net20-api/Properties/AssemblyInfo.cs rename to src/redmine-net-api/Properties/AssemblyInfo.cs index 7705517f..f59d0821 100644 --- a/redmine-net20-api/Properties/AssemblyInfo.cs +++ b/src/redmine-net-api/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("redmine-net20-api")] -[assembly: AssemblyCopyright("Copyright Β© Adrian Popescu 2011 - 2017")] +[assembly: AssemblyCopyright("Copyright Β© Adrian Popescu 2011 - 2019")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/src/redmine-net-api/RedmineConstants.cs b/src/redmine-net-api/RedmineConstants.cs new file mode 100644 index 00000000..fa752ce4 --- /dev/null +++ b/src/redmine-net-api/RedmineConstants.cs @@ -0,0 +1,73 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +namespace Redmine.Net.Api +{ + /// + /// + /// + public static class RedmineConstants + { + /// + /// + /// + internal const string OBSOLETE_TEXT = "In next major release, it will no longer be available."; + /// + /// + /// + public const int DEFAULT_PAGE_SIZE_VALUE = 25; + + /// + /// + /// + public const string CONTENT_TYPE_APPLICATION_JSON = "application/json"; + /// + /// + /// + public const string CONTENT_TYPE_APPLICATION_XML = "application/xml"; + /// + /// + /// + public const string CONTENT_TYPE_APPLICATION_STREAM = "application/octet-stream"; + + /// + /// + /// + public const string IMPERSONATE_HEADER_KEY = "X-Redmine-Switch-User"; + + /// + /// + /// + public const string AUTHORIZATION_HEADER_KEY = "Authorization"; + /// + /// + /// + public const string API_KEY_AUTHORIZATION_HEADER_KEY = "X-Redmine-API-Key"; + + /// + /// + /// + public const string XML = "xml"; + + /// + /// + /// + public const string JSON = "json"; + + internal const string USER_AGENT_HEADER_KEY = "User-Agent"; + internal const string CONTENT_TYPE_HEADER_KEY = "Content-Type"; + } +} \ No newline at end of file diff --git a/redmine-net20-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs similarity index 68% rename from redmine-net20-api/RedmineKeys.cs rename to src/redmine-net-api/RedmineKeys.cs index 82d85b21..b5072aa6 100644 --- a/redmine-net20-api/RedmineKeys.cs +++ b/src/redmine-net-api/RedmineKeys.cs @@ -1,5 +1,5 @@ /* - Copyright 2016 - 2017 Adrian Popescu. + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,6 +20,14 @@ namespace Redmine.Net.Api /// public static class RedmineKeys { + /// + /// + /// + public const string ACCOUNT = "account"; + /// + /// + /// + public const string ACTIVE = "active"; /// /// The activity /// @@ -31,14 +39,30 @@ public static class RedmineKeys /// /// /// + public const string ADMIN = "admin"; + /// + /// + /// public const string ALL = "*"; /// /// /// + public const string ALL_WORDS = "all_words"; + /// + /// + /// + public const string ALLOWED_STATUSES = "allowed_statuses"; + /// + /// + /// public const string API_KEY = "api_key"; /// /// /// + public const string ARCHIVE = "archive"; + /// + /// + /// public const string ASSIGNED_TO = "assigned_to"; /// /// @@ -47,6 +71,10 @@ public static class RedmineKeys /// /// /// + public const string ASSIGNABLE = "assignable"; + /// + /// + /// public const string ATTACHMENT = "attachment"; /// /// @@ -60,6 +88,11 @@ public static class RedmineKeys /// /// public const string AUTH_SOURCE_ID = "auth_source_id"; + + /// + /// + /// + public const string AVATAR_URL = "avatar_url"; /// /// /// @@ -71,11 +104,11 @@ public static class RedmineKeys /// /// /// - public const string CHANGESET = "changeset"; + public const string CHANGE_SET = "changeset"; /// /// /// - public const string CHANGESETS = "changesets"; + public const string CHANGE_SETS = "changesets"; /// /// /// @@ -83,10 +116,18 @@ public static class RedmineKeys /// /// /// + public const string CLOSE = "close"; + /// + /// + /// public const string CLOSED_ON = "closed_on"; /// /// /// + public const string COMMENT = "comment"; + /// + /// + /// public const string COMMENTS = "comments"; /// /// @@ -95,6 +136,10 @@ public static class RedmineKeys /// /// /// + public const string CONTENT = "content"; + /// + /// + /// public const string CONTENT_TYPE = "content_type"; /// /// @@ -103,7 +148,26 @@ public static class RedmineKeys /// /// /// + public const string COPIED_FROM = "copied_from"; + /// + /// + /// + public const string COPIED_TO = "copied_to"; + /// + /// + /// public const string CREATED_ON = "created_on"; + + /// + /// + /// + public const string CURRENT = "current"; + + /// + /// + /// + public const string CURRENT_USER = "current_user"; + /// /// /// @@ -112,10 +176,33 @@ public static class RedmineKeys /// /// public const string CUSTOM_FIELD = "custom_field"; + /// + /// + /// + public const string CUSTOM_FIELD_VALUES = "custom_field_values"; + /// /// /// public const string CUSTOM_FIELDS = "custom_fields"; + + /// + /// + /// + public const string DATE_TIME = "datetime"; + + /// + /// + /// + public const string DEFAULT_ASSIGNEE = "default_assignee"; + /// + /// + /// + public const string DEFAULT_ASSIGNED_TO_ID = "default_assigned_to_id"; + /// + /// + /// + public const string DEFAULT_STATUS = "default_status"; /// /// /// @@ -123,6 +210,14 @@ public static class RedmineKeys /// /// /// + public const string DEFAULT_VERSION = "default_version"; + /// + /// + /// + public const string DEFAULT_VERSION_ID = "default_version_id"; + /// + /// + /// public const string DELAY = "delay"; /// /// @@ -147,6 +242,14 @@ public static class RedmineKeys /// /// /// + public const string DOCUMENT_CATEGORY = "document_category"; + /// + /// + /// + public const string DOCUMENTS = "documents"; + /// + /// + /// public const string DOWNLOADS = "downloads"; /// /// @@ -167,6 +270,22 @@ public static class RedmineKeys /// /// /// + public const string ENABLED_STANDARD_FIELDS = "enabled_standard_fields"; + /// + /// + /// + public const string ENUMERATION_DOCUMENT_CATEGORIES = "enumerations/document_categories"; + /// + /// + /// + public const string ENUMERATION_ISSUE_PRIORITIES = "enumerations/issue_priorities"; + /// + /// + /// + public const string ENUMERATION_TIME_ENTRY_ACTIVITIES = "enumerations/time_entry_activities"; + /// + /// + /// public const string ERROR = "error"; /// /// @@ -179,6 +298,10 @@ public static class RedmineKeys /// /// /// + public const string FIELD = "field"; + /// + /// + /// public const string FIELD_FORMAT = "field_format"; /// /// @@ -187,15 +310,20 @@ public static class RedmineKeys /// /// /// - public const string FILENAME = "filename"; + public const string FILE_NAME = "filename"; + /// + /// + /// + public const string FILE_SIZE = "filesize"; + /// /// /// - public const string FILESIZE = "filesize"; + public const string FILES = "files"; /// /// /// - public const string FIRSTNAME = "firstname"; + public const string FIRST_NAME = "firstname"; /// /// /// @@ -207,6 +335,10 @@ public static class RedmineKeys /// /// /// + public const string GENERATE_PASSWORD = "generate_password"; + /// + /// + /// public const string GROUP = "group"; /// /// @@ -239,6 +371,10 @@ public static class RedmineKeys /// /// /// + public const string INDEX = "index"; + /// + /// + /// public const string INHERITED = "inherited"; /// /// @@ -263,6 +399,14 @@ public static class RedmineKeys /// /// /// + public const string ISSUE_CUSTOM_FIELDS = "issue_custom_fields"; + /// + /// + /// + public const string ISSUE_CUSTOM_FIELD_IDS = "issue_custom_field_ids"; + /// + /// + /// public const string ISSUE_ID = "issue_id"; /// /// @@ -279,7 +423,16 @@ public static class RedmineKeys /// /// /// + public const string ISSUE_STATUSES = "issue_statuses"; + /// + /// + /// public const string ISSUE_TO_ID = "issue_to_id"; + /// + /// + /// + public const string ISSUES_VISIBILITY = "issues_visibility"; + /// /// /// @@ -319,7 +472,11 @@ public static class RedmineKeys /// /// /// - public const string LASTNAME = "lastname"; + public const string LABEL = "label"; + /// + /// + /// + public const string LAST_NAME = "lastname"; /// /// /// @@ -355,6 +512,10 @@ public static class RedmineKeys /// /// /// + public const string MESSAGES = "messages"; + /// + /// + /// public const string MIN_LENGTH = "min_length"; /// /// @@ -363,7 +524,11 @@ public static class RedmineKeys /// /// /// - public const string MUST_CHANGE_PASSWD = "must_change_passwd"; + public const string MUST_CHANGE_PASSWORD = "must_change_passwd"; + /// + /// + /// + public const string MY = "my"; /// /// /// @@ -391,6 +556,10 @@ public static class RedmineKeys /// /// /// + public const string OPEN_ISSUES = "open_issues"; + /// + /// + /// public const string PARENT = "parent"; /// /// @@ -407,6 +576,10 @@ public static class RedmineKeys /// /// /// + public const string PASSWORD_CHANGED_ON = "passwd_changed_on"; + /// + /// + /// public const string PERMISSION = "permission"; /// /// @@ -451,10 +624,22 @@ public static class RedmineKeys /// /// /// + public const string Q = "q"; + /// + /// + /// public const string QUERY = "query"; /// /// /// + public const string QUERIES = "queries"; + /// + /// + /// + public const string REASSIGN_TO_ID = "reassign_to_id"; + /// + /// + /// public const string REGEXP = "regexp"; /// /// @@ -471,10 +656,26 @@ public static class RedmineKeys /// /// /// + public const string REOPEN = "reopen"; + /// + /// + /// + public const string REPOSITORY = "repository"; + /// + /// + /// + public const string RESULT = "result"; + /// + /// + /// public const string REVISION = "revision"; /// /// /// + public const string REVISIONS = "revisions"; + /// + /// + /// public const string ROLE = "role"; /// /// @@ -491,10 +692,22 @@ public static class RedmineKeys /// /// /// + public const string SCOPE = "scope"; + /// + /// + /// public const string SEARCHABLE = "searchable"; /// /// /// + public const string SEND_INFORMATION = "send_information"; + /// + /// + /// + public const string SEARCH = "search"; + /// + /// + /// public const string SHARING = "sharing"; /// /// @@ -527,7 +740,7 @@ public static class RedmineKeys /// /// /// - public const string SUBPROJECT_ID = "subproject_id"; + public const string SUB_PROJECT_ID = "subproject_id"; /// /// /// @@ -543,6 +756,10 @@ public static class RedmineKeys /// /// /// + public const string TIME_ENTRIES = "time_entries"; + /// + /// + /// public const string TIME_ENTRY_ACTIVITIES = "time_entry_activities"; /// /// @@ -551,10 +768,22 @@ public static class RedmineKeys /// /// /// + public const string TIME_ENTRIES_VISIBILITY = "time_entries_visibility"; + /// + /// + /// public const string TITLE = "title"; /// /// /// + public const string TITLES_ONLY = "titles_only"; + /// + /// + /// + public const string THUMBNAIL_URL = "thumbnail_url"; + /// + /// + /// public const string TOKEN = "token"; /// /// @@ -564,12 +793,10 @@ public static class RedmineKeys /// /// public const string TOTAL_ESTIMATED_HOURS = "total_estimated_hours"; - /// /// /// public const string TOTAL_SPENT_HOURS = "total_spent_hours"; - /// /// /// @@ -586,6 +813,19 @@ public static class RedmineKeys /// /// public const string TRACKER_IDS = "tracker_ids"; + + /// + /// + /// + public const string TYPE = "type"; + /// + /// + /// + public const string TWO_FA_SCHEME = "twofa_scheme"; + /// + /// + /// + public const string UNARCHIVE = "unarchive"; /// /// /// @@ -593,6 +833,10 @@ public static class RedmineKeys /// /// /// + public const string UPDATED_BY = "updated_by"; + /// + /// + /// public const string UPLOAD = "upload"; /// /// @@ -601,6 +845,10 @@ public static class RedmineKeys /// /// /// + public const string URL = "url"; + /// + /// + /// public const string USER = "user"; /// /// @@ -617,6 +865,10 @@ public static class RedmineKeys /// /// /// + public const string USERS_VISIBILITY = "users_visibility"; + /// + /// + /// public const string VALUE = "value"; /// /// @@ -629,6 +881,10 @@ public static class RedmineKeys /// /// /// + public const string VERSIONS = "versions"; + /// + /// + /// public const string VISIBLE = "visible"; /// /// @@ -645,14 +901,18 @@ public static class RedmineKeys /// /// /// - public const string WIKI_PAGE = "wiki_page"; + public const string WIKI = "wiki"; + /// + /// + /// + public const string WIKI_PAGE = "wiki_page"; + /// + /// + /// + public const string WIKI_PAGE_TITLE = "wiki_page_title"; /// /// /// public const string WIKI_PAGES = "wiki_pages"; - /// - /// - /// - public const string REASSIGN_TO_ID = "reassign_to_id"; } } \ No newline at end of file diff --git a/src/redmine-net-api/RedmineManager.Async.cs b/src/redmine-net-api/RedmineManager.Async.cs new file mode 100644 index 00000000..bf48328e --- /dev/null +++ b/src/redmine-net-api/RedmineManager.Async.cs @@ -0,0 +1,251 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +#if !(NET20) +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Threading; +using System.Threading.Tasks; +using Redmine.Net.Api.Common; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Http.Extensions; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Net.Internal; +using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Types; +#if!(NET45_OR_GREATER || NETCOREAPP) +using TaskExtensions = Redmine.Net.Api.Extensions.TaskExtensions; +#endif + +namespace Redmine.Net.Api; + +public partial class RedmineManager: IRedmineManagerAsync +{ + /// + public async Task CountAsync(RequestOptions requestOptions, CancellationToken cancellationToken = default) + where T : class, new() + { + var totalCount = 0; + + requestOptions ??= new RequestOptions(); + + requestOptions.QueryString.AddPagingParameters(pageSize: 1, offset: 0); + + var tempResult = await GetPagedAsync(requestOptions, cancellationToken).ConfigureAwait(false); + if (tempResult != null) + { + totalCount = tempResult.TotalItems; + } + + return totalCount; + } + + /// + public async Task> GetPagedAsync(RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new() + { + var url = RedmineApiUrls.GetListFragment(requestOptions); + + var response = await ApiClient.GetAsync(url, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeToPagedResults(Serializer); + } + + + /// + public async Task> GetAsync(RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new() + { + int pageSize = 0, offset = 0; + var isLimitSet = false; + List resultList = null; + + var baseRequestOptions = requestOptions != null ? requestOptions.Clone() : new RequestOptions(); + if (baseRequestOptions.QueryString == null) + { + baseRequestOptions.QueryString = new NameValueCollection(); + } + else + { + isLimitSet = int.TryParse(baseRequestOptions.QueryString[RedmineKeys.LIMIT], out pageSize); + int.TryParse(baseRequestOptions.QueryString[RedmineKeys.OFFSET], out offset); + } + + if (pageSize == default) + { + pageSize = _redmineManagerOptions.PageSize > 0 + ? _redmineManagerOptions.PageSize + : RedmineConstants.DEFAULT_PAGE_SIZE_VALUE; + baseRequestOptions.QueryString.Set(RedmineKeys.LIMIT, pageSize.ToInvariantString()); + } + + var hasOffset = TypesWithOffset.ContainsKey(typeof(T)); + if (hasOffset) + { + var firstPageOptions = baseRequestOptions.Clone(); + firstPageOptions.QueryString.Set(RedmineKeys.OFFSET, offset.ToInvariantString()); + var firstPage = await GetPagedAsync(firstPageOptions, cancellationToken).ConfigureAwait(false); + + if (firstPage == null || firstPage.Items == null) + { + return null; + } + + var totalCount = isLimitSet ? pageSize : firstPage.TotalItems; + resultList = new List(firstPage.Items); + + var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); + var remainingPages = totalPages - 1 - (offset / pageSize); + if (remainingPages <= 0) + { + return resultList; + } + + using (var semaphore = new SemaphoreSlim(MAX_CONCURRENT_TASKS)) + { + var pageFetchTasks = new List>>(); + for (int page = 1; page <= remainingPages; page++) + { + await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + var pageOffset = (page * pageSize) + offset; + var pageRequestOptions = baseRequestOptions.Clone(); + pageRequestOptions.QueryString.Set(RedmineKeys.OFFSET, pageOffset.ToInvariantString()); + pageFetchTasks.Add(GetPagedInternalAsync(semaphore, pageRequestOptions, cancellationToken)); + } + + var pageResults = await + #if(NET45_OR_GREATER || NETCOREAPP) + Task.WhenAll(pageFetchTasks) + #else + TaskExtensions.WhenAll(pageFetchTasks) + #endif + .ConfigureAwait(false); + + foreach (var pageResult in pageResults) + { + if (pageResult?.Items == null) + { + continue; + } + resultList.AddRange(pageResult.Items); + } + } + } + else + { + var result = await GetPagedAsync(baseRequestOptions, cancellationToken: cancellationToken) + .ConfigureAwait(false); + if (result?.Items != null) + { + return new List(result.Items); + } + } + + return resultList; + } + + /// + public async Task GetAsync(string id, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new() + { + var url = RedmineApiUrls.GetFragment(id); + + var response = await ApiClient.GetAsync(url, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeTo(Serializer); + } + + /// + public async Task CreateAsync(T entity, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new() + { + return await CreateAsync(entity, null, requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + public async Task CreateAsync(T entity, string ownerId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new() + { + var url = RedmineApiUrls.CreateEntityFragment(ownerId); + + var payload = Serializer.Serialize(entity); + + var response = await ApiClient.CreateAsync(url, payload, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeTo(Serializer); + } + + /// + public async Task UpdateAsync(string id, T entity, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new() + { + var url = RedmineApiUrls.UpdateFragment(id); + + var payload = Serializer.Serialize(entity); + + payload = payload.ReplaceEndings(); + + await ApiClient.UpdateAsync(url, payload, requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + public async Task DeleteAsync(string id, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new() + { + var url = RedmineApiUrls.DeleteFragment(id); + + await ApiClient.DeleteAsync(url, requestOptions, cancellationToken).ConfigureAwait((false)); + } + + /// + public async Task UploadFileAsync(byte[] data, string fileName = null, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var url = RedmineApiUrls.UploadFragment(fileName); + + var response = await ApiClient.UploadFileAsync(url, data, requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false); + + return response.DeserializeTo(Serializer); + } + + /// + public async Task DownloadFileAsync(string address, RequestOptions requestOptions = null, IProgress progress = null, CancellationToken cancellationToken = default) + { + var response = await ApiClient.DownloadAsync(address, requestOptions, progress, cancellationToken: cancellationToken).ConfigureAwait(false); + return response.Content; + } + + private const int MAX_CONCURRENT_TASKS = 3; + + private async Task> GetPagedInternalAsync(SemaphoreSlim semaphore, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new() + { + try + { + var url = RedmineApiUrls.GetListFragment(requestOptions); + + var response = await ApiClient.GetAsync(url, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeToPagedResults(Serializer); + } + finally + { + semaphore.Release(); + } + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs new file mode 100644 index 00000000..ca062ef8 --- /dev/null +++ b/src/redmine-net-api/RedmineManager.cs @@ -0,0 +1,355 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Net; +using Redmine.Net.Api.Common; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Http.Clients.WebClient; +using Redmine.Net.Api.Http.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Logging; +#if NET40_OR_GREATER || NET +using Redmine.Net.Api.Http.Clients.HttpClient; +#endif +using Redmine.Net.Api.Net.Internal; +using Redmine.Net.Api.Options; +using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api +{ + /// + /// The main class to access Redmine API. + /// + public partial class RedmineManager : IRedmineManager + { + private readonly RedmineManagerOptions _redmineManagerOptions; + + internal IRedmineSerializer Serializer { get; } + internal RedmineApiUrls RedmineApiUrls { get; } + internal IRedmineApiClient ApiClient { get; } + internal IRedmineLogger Logger { get; } + + /// + /// + /// + /// + /// + public RedmineManager(RedmineManagerOptionsBuilder optionsBuilder) + { + ArgumentNullThrowHelper.ThrowIfNull(optionsBuilder, nameof(optionsBuilder)); + + _redmineManagerOptions = optionsBuilder.Build(); + + Logger = _redmineManagerOptions.Logger; + Serializer = _redmineManagerOptions.Serializer; + RedmineApiUrls = new RedmineApiUrls(_redmineManagerOptions.Serializer.Format); + + ApiClient = +#if NET40_OR_GREATER || NET + _redmineManagerOptions.ApiClientOptions switch + { + RedmineWebClientOptions => CreateWebClient(_redmineManagerOptions), + RedmineHttpClientOptions => CreateHttpClient(_redmineManagerOptions), + }; +#else + CreateWebClient(_redmineManagerOptions); +#endif + } + + private static InternalRedmineApiWebClient CreateWebClient(RedmineManagerOptions options) + { + if (options.ClientFunc != null) + { + return new InternalRedmineApiWebClient(options.ClientFunc, options); + } + + ApplyServiceManagerSettings(options.WebClientOptions); +#pragma warning disable SYSLIB0014 + options.WebClientOptions.SecurityProtocolType ??= ServicePointManager.SecurityProtocol; +#pragma warning restore SYSLIB0014 + + return new InternalRedmineApiWebClient(options); + } +#if NET40_OR_GREATER || NET + private InternalRedmineApiHttpClient CreateHttpClient(RedmineManagerOptions options) + { + return options.HttpClient != null + ? new InternalRedmineApiHttpClient(options.HttpClient, options) + : new InternalRedmineApiHttpClient(_redmineManagerOptions); + } +#endif + + private static void ApplyServiceManagerSettings(RedmineWebClientOptions options) + { + if (options == null) + { + return; + } + + if (options.SecurityProtocolType.HasValue) + { + ServicePointManager.SecurityProtocol = options.SecurityProtocolType.Value; + } + + if (options.DefaultConnectionLimit.HasValue) + { + ServicePointManager.DefaultConnectionLimit = options.DefaultConnectionLimit.Value; + } + + if (options.DnsRefreshTimeout.HasValue) + { + ServicePointManager.DnsRefreshTimeout = options.DnsRefreshTimeout.Value; + } + + if (options.EnableDnsRoundRobin.HasValue) + { + ServicePointManager.EnableDnsRoundRobin = options.EnableDnsRoundRobin.Value; + } + + if (options.MaxServicePoints.HasValue) + { + ServicePointManager.MaxServicePoints = options.MaxServicePoints.Value; + } + + if (options.MaxServicePointIdleTime.HasValue) + { + ServicePointManager.MaxServicePointIdleTime = options.MaxServicePointIdleTime.Value; + } + +#if(NET46_OR_GREATER || NET) + if (options.ReusePort.HasValue) + { + ServicePointManager.ReusePort = options.ReusePort.Value; + } +#endif + #if NEFRAMEWORK + if (options.CheckCertificateRevocationList) + { + ServicePointManager.CheckCertificateRevocationList = true; + } +#endif + } + + /// + public int Count(RequestOptions requestOptions = null) + where T : class, new() + { + var totalCount = 0; + const int PAGE_SIZE = 1; + const int OFFSET = 0; + + requestOptions ??= new RequestOptions(); + + requestOptions.QueryString = requestOptions.QueryString.AddPagingParameters(PAGE_SIZE, OFFSET); + + var tempResult = GetPaginated(requestOptions); + + if (tempResult != null) + { + totalCount = tempResult.TotalItems; + } + + return totalCount; + } + + /// + public T Get(string id, RequestOptions requestOptions = null) + where T : class, new() + { + var url = RedmineApiUrls.GetFragment(id); + + var response = ApiClient.Get(url, requestOptions); + + return response.DeserializeTo(Serializer); + } + + /// + public List Get(RequestOptions requestOptions = null) + where T : class, new() + { + var uri = RedmineApiUrls.GetListFragment(requestOptions); + + return GetInternal(uri, requestOptions); + } + + /// + public PagedResults GetPaginated(RequestOptions requestOptions = null) + where T : class, new() + { + var url = RedmineApiUrls.GetListFragment(requestOptions); + + return GetPaginatedInternal(url, requestOptions); + } + + /// + public T Create(T entity, string ownerId = null, RequestOptions requestOptions = null) + where T : class, new() + { + var url = RedmineApiUrls.CreateEntityFragment(ownerId); + + var payload = Serializer.Serialize(entity); + + var response = ApiClient.Create(url, payload, requestOptions); + + return response.DeserializeTo(Serializer); + } + + /// + public void Update(string id, T entity, string projectId = null, RequestOptions requestOptions = null) + where T : class, new() + { + var url = RedmineApiUrls.UpdateFragment(id); + + var payload = Serializer.Serialize(entity); + + payload = payload.ReplaceEndings(); + + ApiClient.Update(url, payload, requestOptions); + } + + /// + public void Delete(string id, RequestOptions requestOptions = null) + where T : class, new() + { + var url = RedmineApiUrls.DeleteFragment(id); + + ApiClient.Delete(url, requestOptions); + } + + /// + public Upload UploadFile(byte[] data, string fileName = null) + { + var url = RedmineApiUrls.UploadFragment(fileName); + + var response = ApiClient.Upload(url, data); + + return response.DeserializeTo(Serializer); + } + + /// + public byte[] DownloadFile(string address, IProgress progress = null) + { + var response = ApiClient.Download(address, progress: progress); + + return response.Content; + } + + /// + /// + /// + /// + /// + /// + /// + internal List GetInternal(string uri, RequestOptions requestOptions = null) + where T : class, new() + { + int pageSize = 0, offset = 0; + var isLimitSet = false; + List resultList = null; + + requestOptions ??= new RequestOptions(); + + if (requestOptions.QueryString == null) + { + requestOptions.QueryString = new NameValueCollection(); + } + else + { + isLimitSet = int.TryParse(requestOptions.QueryString[RedmineKeys.LIMIT], out pageSize); + int.TryParse(requestOptions.QueryString[RedmineKeys.OFFSET], out offset); + } + + if (pageSize == default) + { + pageSize = _redmineManagerOptions.PageSize > 0 ? _redmineManagerOptions.PageSize : RedmineConstants.DEFAULT_PAGE_SIZE_VALUE; + requestOptions.QueryString.Set(RedmineKeys.LIMIT, pageSize.ToInvariantString()); + } + + var hasOffset = TypesWithOffset.ContainsKey(typeof(T)); + if (hasOffset) + { + int totalCount; + do + { + requestOptions.QueryString.Set(RedmineKeys.OFFSET, offset.ToInvariantString()); + + var tempResult = GetPaginatedInternal(uri, requestOptions); + + totalCount = isLimitSet ? pageSize : tempResult.TotalItems; + + if (tempResult?.Items != null) + { + if (resultList == null) + { + resultList = new List(tempResult.Items); + } + else + { + resultList.AddRange(tempResult.Items); + } + } + + offset += pageSize; + } + while (offset < totalCount); + } + else + { + var result = GetPaginatedInternal(uri, requestOptions); + if (result?.Items != null) + { + return new List(result.Items); + } + } + + return resultList; + } + + /// + /// + /// + /// + /// + /// + /// + internal PagedResults GetPaginatedInternal(string uri = null, RequestOptions requestOptions = null) + where T : class, new() + { + uri = uri.IsNullOrWhiteSpace() ? RedmineApiUrls.GetListFragment(requestOptions) : uri; + + var response= ApiClient.Get(uri, requestOptions); + + return response.DeserializeToPagedResults(Serializer); + } + + internal static readonly Dictionary TypesWithOffset = new Dictionary{ + {typeof(Issue), true}, + {typeof(Project), true}, + {typeof(User), true}, + {typeof(News), true}, + {typeof(Query), true}, + {typeof(TimeEntry), true}, + {typeof(ProjectMembership), true}, + {typeof(Search), true} + }; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/SearchFilterBuilder.cs b/src/redmine-net-api/SearchFilterBuilder.cs new file mode 100644 index 00000000..8f376aa6 --- /dev/null +++ b/src/redmine-net-api/SearchFilterBuilder.cs @@ -0,0 +1,187 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections.Specialized; +using Redmine.Net.Api.Http.Extensions; + +namespace Redmine.Net.Api +{ + /// + /// + /// + // ReSharper disable once ClassNeverInstantiated.Global + public sealed class SearchFilterBuilder + { + /// + /// search scope condition + /// + /// + public SearchScope? Scope + { + get => _scope; + set + { + _scope = value; + if (_scope != null) + { + _internalScope = _scope switch + { + SearchScope.All => "all", + SearchScope.MyProject => "my_project", + SearchScope.SubProjects => "subprojects", + _ => throw new ArgumentOutOfRangeException(nameof(value)) + }; + } + } + } + + /// + /// + /// + public bool? AllWords { get; set; } + + /// + /// + /// + public bool? TitlesOnly { get; set; } + + /// + /// + /// + public bool? IncludeIssues{ get; set; } + + /// + /// + /// + public bool? IncludeNews{ get; set; } + + /// + /// + /// + public bool? IncludeDocuments{ get; set; } + + /// + /// + /// + public bool? IncludeChangeSets{ get; set; } + + /// + /// + /// + public bool? IncludeWikiPages{ get; set; } + + /// + /// + /// + public bool? IncludeMessages{ get; set; } + + /// + /// + /// + public bool? IncludeProjects{ get; set; } + + /// + /// filtered by open issues. + /// + public bool? OpenIssues{ get; set; } + + /// + /// + public SearchAttachment? Attachments + { + get => _attachments; + set + { + _attachments = value; + if (_attachments != null) + { + _internalAttachments = _attachments switch + { + SearchAttachment.OnlyInAttachment => "only", + SearchAttachment.InDescription => "0", + SearchAttachment.InDescriptionAndAttachment => "1", + _ => throw new ArgumentOutOfRangeException(nameof(Attachments)) + }; + } + } + } + + private string _internalScope; + private string _internalAttachments; + private SearchAttachment? _attachments; + private SearchScope? _scope; + + /// + /// + /// + public NameValueCollection Build(NameValueCollection sb) + { + sb.AddIfNotNull(RedmineKeys.SCOPE,_internalScope); + sb.AddIfNotNull(RedmineKeys.PROJECTS, IncludeProjects); + sb.AddIfNotNull(RedmineKeys.OPEN_ISSUES, OpenIssues); + sb.AddIfNotNull(RedmineKeys.MESSAGES, IncludeMessages); + sb.AddIfNotNull(RedmineKeys.WIKI_PAGES, IncludeWikiPages); + sb.AddIfNotNull(RedmineKeys.CHANGE_SETS, IncludeChangeSets); + sb.AddIfNotNull(RedmineKeys.DOCUMENTS, IncludeDocuments); + sb.AddIfNotNull(RedmineKeys.NEWS, IncludeNews); + sb.AddIfNotNull(RedmineKeys.ISSUES, IncludeIssues); + sb.AddIfNotNull(RedmineKeys.TITLES_ONLY, TitlesOnly); + sb.AddIfNotNull(RedmineKeys.ALL_WORDS, AllWords); + sb.AddIfNotNull(RedmineKeys.ATTACHMENTS, _internalAttachments); + + return sb; + } + } + + /// + /// + /// + public enum SearchScope + { + /// + /// all projects + /// + All, + /// + /// assigned projects + /// + MyProject, + /// + /// include subproject + /// + SubProjects + } + + /// + /// + /// + public enum SearchAttachment + { + /// + /// search only in description + /// + InDescription = 0, + /// + /// search by description and attachment + /// + InDescriptionAndAttachment, + /// + /// search only in attachment + /// + OnlyInAttachment + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/IRedmineSerializer.cs b/src/redmine-net-api/Serialization/IRedmineSerializer.cs new file mode 100644 index 00000000..eef94866 --- /dev/null +++ b/src/redmine-net-api/Serialization/IRedmineSerializer.cs @@ -0,0 +1,51 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using Redmine.Net.Api.Common; + +namespace Redmine.Net.Api.Serialization +{ + /// + /// Serialization interface that supports serialize and deserialize methods. + /// + internal interface IRedmineSerializer + { + /// + /// Gets the application format this serializer supports (e.g. "json", "xml"). + /// + string Format { get; } + + /// + /// + /// + string ContentType { get; } + + /// + /// Serializes the specified object into a string. + /// + string Serialize(T obj) where T : class; + + /// + /// Deserializes the string into a PageResult of T object. + /// + PagedResults DeserializeToPagedResults(string response) where T : class, new(); + + /// + /// Deserializes the string into an object. + /// + T Deserialize(string input) where T : new(); + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/Json/Extensions/JsonReaderExtensions.cs b/src/redmine-net-api/Serialization/Json/Extensions/JsonReaderExtensions.cs new file mode 100644 index 00000000..8dc4e76e --- /dev/null +++ b/src/redmine-net-api/Serialization/Json/Extensions/JsonReaderExtensions.cs @@ -0,0 +1,98 @@ +ο»Ώ/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Redmine.Net.Api.Exceptions; + +namespace Redmine.Net.Api.Serialization.Json.Extensions +{ + /// + /// + /// + public static partial class JsonExtensions + { + /// + /// + /// + /// + /// + public static int ReadAsInt(this JsonReader reader) + { + return reader.ReadAsInt32().GetValueOrDefault(); + } + + /// + /// + /// + /// + /// + public static bool ReadAsBool(this JsonReader reader) + { + return reader.ReadAsBoolean().GetValueOrDefault(); + } + + /// + /// + /// + /// + /// + /// + /// + public static List ReadAsCollection(this JsonReader reader, bool readInnerArray = false) where T : class + { + var isJsonSerializable = typeof(IJsonSerializable).IsAssignableFrom(typeof(T)); + + if (!isJsonSerializable) + { + throw new RedmineException($"Entity of type '{typeof(T)}' should implement IJsonSerializable."); + } + + List collection = null; + + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndArray) + { + break; + } + + if (readInnerArray) + { + if (reader.TokenType == JsonToken.PropertyName) + { + break; + } + } + + if (reader.TokenType == JsonToken.StartArray) + { + continue; + } + + var entity = Activator.CreateInstance(); + + ((IJsonSerializable)entity).ReadJson(reader); + + collection ??= new List(); + collection.Add(entity); + } + + return collection; + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/Json/Extensions/JsonWriterExtensions.cs b/src/redmine-net-api/Serialization/Json/Extensions/JsonWriterExtensions.cs new file mode 100644 index 00000000..f3df1629 --- /dev/null +++ b/src/redmine-net-api/Serialization/Json/Extensions/JsonWriterExtensions.cs @@ -0,0 +1,310 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using Newtonsoft.Json; +using Redmine.Net.Api.Common; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.Serialization.Json.Extensions +{ + /// + /// + /// + public static partial class JsonExtensions + { + /// + /// + /// + /// + /// + /// + public static void WriteIdIfNotNull(this JsonWriter jsonWriter, string tag, IdentifiableName value) + { + if (value == null) + { + return; + } + + jsonWriter.WritePropertyName(tag); + jsonWriter.WriteValue(value.Id); + } + + /// + /// Writes if not default or null. + /// + /// + /// The writer. + /// The value. + /// The property name. + public static void WriteIfNotDefaultOrNull(this JsonWriter writer, string elementName, T value) + { + if (EqualityComparer.Default.Equals(value, default)) + { + return; + } + + writer.WriteProperty(elementName, typeof(T) == typeof(bool) ? value.ToString().ToLowerInv() : value.ToString()); + } + + /// + /// Writes the boolean value + /// + /// The writer. + /// The value. + /// The property name. + public static void WriteBoolean(this JsonWriter writer, string elementName, bool value) + { + writer.WriteProperty(elementName, value.ToInvariantString()); + } + + /// + /// + /// + /// + /// + /// + /// + public static void WriteIdOrEmpty(this JsonWriter jsonWriter, string tag, IdentifiableName ident, string emptyValue = null) + { + jsonWriter.WriteProperty(tag, ident != null ? ident.Id.ToInvariantString() : emptyValue); + } + + /// + /// + /// + /// + /// + /// + /// + public static void WriteDateOrEmpty(this JsonWriter jsonWriter, string tag, DateTime? val, string dateFormat = "yyyy-MM-dd") + { + if (!val.HasValue || val.Value.Equals(default)) + { + jsonWriter.WriteProperty(tag, string.Empty); + } + else + { + jsonWriter.WriteProperty(tag, val.Value.ToString(dateFormat, CultureInfo.InvariantCulture)); + } + } + + /// + /// + /// + /// + /// + /// + /// + public static void WriteValueOrEmpty(this JsonWriter jsonWriter, string tag, T? val) where T : struct + { + if (!val.HasValue || EqualityComparer.Default.Equals(val.Value, default)) + { + jsonWriter.WriteProperty(tag, string.Empty); + } + else + { + jsonWriter.WriteProperty(tag, val.Value); + } + } + + /// + /// + /// + /// + /// + /// + /// + public static void WriteValueOrDefault(this JsonWriter jsonWriter, string tag, T? val) where T : struct + { + jsonWriter.WriteProperty(tag, val ?? default); + } + + /// + /// + /// + /// + /// + /// + public static void WriteProperty(this JsonWriter jsonWriter, string tag, object value) + { + jsonWriter.WritePropertyName(tag); + jsonWriter.WriteValue(value); + } + + /// + /// + /// + /// + /// + /// + public static void WriteProperty(this JsonWriter jsonWriter, string tag, int value) + { + jsonWriter.WritePropertyName(tag); + jsonWriter.WriteValue(value); + } + + /// + /// + /// + /// + /// + /// + public static void WriteProperty(this JsonWriter jsonWriter, string tag, bool value) + { + jsonWriter.WritePropertyName(tag); + jsonWriter.WriteValue(value); + } + + /// + /// + /// + /// + /// + /// + public static void WriteRepeatableElement(this JsonWriter jsonWriter, string tag, IEnumerable collection) + { + if (collection == null) + { + return; + } + + foreach (var value in collection) + { + jsonWriter.WriteProperty(tag, value.Value); + } + } + + /// + /// + /// + /// + /// + /// + public static void WriteArrayIds(this JsonWriter jsonWriter, string tag, IEnumerable collection) + { + if (collection == null) + { + return; + } + + jsonWriter.WritePropertyName(tag); + jsonWriter.WriteStartArray(); + + var sb = new StringBuilder(); + + foreach (var identifiableName in collection) + { + sb.Append(identifiableName.Id.ToInvariantString()).Append(','); + } + + if (sb.Length > 1) + { + sb.Length -= 1; + } + + jsonWriter.WriteValue(sb.ToString()); + + sb.Length = 0; + + jsonWriter.WriteEndArray(); + } + + /// + /// + /// + /// + /// + /// + public static void WriteArrayNames(this JsonWriter jsonWriter, string tag, IEnumerable collection) + { + if (collection == null) + { + return; + } + + jsonWriter.WritePropertyName(tag); + jsonWriter.WriteStartArray(); + + var sb = new StringBuilder(); + + foreach (var identifiableName in collection) + { + sb.Append(identifiableName.Name).Append(','); + } + + if (sb.Length > 1) + { + sb.Length -= 1; + } + + jsonWriter.WriteValue(sb.ToString()); + + sb.Length = 0; + + jsonWriter.WriteEndArray(); + } + + /// + /// + /// + /// + /// + /// + /// + public static void WriteArray(this JsonWriter jsonWriter, string tag, ICollection collection) where T : IJsonSerializable + { + if (collection == null) + { + return; + } + + jsonWriter.WritePropertyName(tag); + jsonWriter.WriteStartArray(); + + foreach (var item in collection) + { + item.WriteJson(jsonWriter); + } + + jsonWriter.WriteEndArray(); + } + + /// + /// + /// + /// + /// + /// + public static void WriteListAsProperty(this JsonWriter jsonWriter, string tag, ICollection collection) + { + if (collection == null) + { + return; + } + + foreach (var item in collection) + { + jsonWriter.WriteProperty(tag, item); + } + } + } +} \ No newline at end of file diff --git a/redmine-net20-api/Types/ProjectTracker.cs b/src/redmine-net-api/Serialization/Json/IJsonSerializable.cs old mode 100755 new mode 100644 similarity index 64% rename from redmine-net20-api/Types/ProjectTracker.cs rename to src/redmine-net-api/Serialization/Json/IJsonSerializable.cs index 6a1abca7..bf6b25b7 --- a/redmine-net20-api/Types/ProjectTracker.cs +++ b/src/redmine-net-api/Serialization/Json/IJsonSerializable.cs @@ -1,5 +1,5 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. +/* + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,28 +14,24 @@ You may obtain a copy of the License at limitations under the License. */ -using System.Xml.Serialization; +using Newtonsoft.Json; -namespace Redmine.Net.Api.Types +namespace Redmine.Net.Api.Serialization.Json { /// /// /// - [XmlRoot(RedmineKeys.TRACKER)] - public class ProjectTracker : IdentifiableName, IValue + public interface IJsonSerializable { /// /// /// - public string Value{get{return Id.ToString ();}} - + /// + void WriteJson(JsonWriter writer); /// /// /// - /// - public override string ToString () - { - return string.Format ("[ProjectTracker: {0}]", base.ToString()); - } + /// + void ReadJson(JsonReader reader); } } \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/Json/JsonObject.cs b/src/redmine-net-api/Serialization/Json/JsonObject.cs new file mode 100644 index 00000000..38e70807 --- /dev/null +++ b/src/redmine-net-api/Serialization/Json/JsonObject.cs @@ -0,0 +1,64 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.Serialization.Json +{ + /// + /// + /// + public sealed class JsonObject : IDisposable + { + private readonly bool hasRoot; + + /// + /// + /// + /// + /// + public JsonObject(JsonWriter writer, string root = null) + { + Writer = writer; + Writer.WriteStartObject(); + + if (root.IsNullOrWhiteSpace()) + { + return; + } + + hasRoot = true; + Writer.WritePropertyName(root); + Writer.WriteStartObject(); + } + + private JsonWriter Writer { get; } + + /// + /// + /// + public void Dispose() + { + Writer.WriteEndObject(); + if (hasRoot) + { + Writer.WriteEndObject(); + } + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs b/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs new file mode 100644 index 00000000..bbc2c488 --- /dev/null +++ b/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs @@ -0,0 +1,155 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Newtonsoft.Json; +using Redmine.Net.Api.Common; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Serialization.Json.Extensions; + +namespace Redmine.Net.Api.Serialization.Json +{ + internal sealed class JsonRedmineSerializer : IRedmineSerializer + { + private static void EnsureJsonSerializable() + { + if (!typeof(IJsonSerializable).IsAssignableFrom(typeof(T))) + { + throw new RedmineException($"Entity of type '{typeof(T)}' should implement IJsonSerializable."); + } + } + + public T Deserialize(string jsonResponse) where T : new() + { + SerializationHelper.EnsureDeserializationInputIsNotNullOrWhiteSpace(jsonResponse, nameof(jsonResponse), typeof(T)); + + EnsureJsonSerializable(); + + using var stringReader = new StringReader(jsonResponse); + using var jsonReader = new JsonTextReader(stringReader); + var obj = Activator.CreateInstance(); + + if (jsonReader.Read() && jsonReader.Read()) + { + ((IJsonSerializable)obj).ReadJson(jsonReader); + } + + return obj; + } + + public PagedResults DeserializeToPagedResults(string jsonResponse) where T : class, new() + { + SerializationHelper.EnsureDeserializationInputIsNotNullOrWhiteSpace(jsonResponse, nameof(jsonResponse), typeof(T)); + + using var sr = new StringReader(jsonResponse); + using var reader = new JsonTextReader(sr); + var total = 0; + var offset = 0; + var limit = 0; + List list = null; + + while (reader.Read()) + { + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.TOTAL_COUNT: + total = reader.ReadAsInt32().GetValueOrDefault(); + break; + case RedmineKeys.OFFSET: + offset = reader.ReadAsInt32().GetValueOrDefault(); + break; + case RedmineKeys.LIMIT: + limit = reader.ReadAsInt32().GetValueOrDefault(); + break; + default: + list = reader.ReadAsCollection(); + break; + } + } + + return new PagedResults(list, total, offset, limit); + } + + #pragma warning disable CA1822 + public int Count(string jsonResponse) where T : class, new() + { + SerializationHelper.EnsureDeserializationInputIsNotNullOrWhiteSpace(jsonResponse, nameof(jsonResponse), typeof(T)); + + using var sr = new StringReader(jsonResponse); + using var reader = new JsonTextReader(sr); + var total = 0; + + while (reader.Read()) + { + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + if (reader.Value is RedmineKeys.TOTAL_COUNT) + { + total = reader.ReadAsInt32().GetValueOrDefault(); + return total; + } + } + + return total; + } + #pragma warning restore CA1822 + + public string Format { get; } = RedmineConstants.JSON; + + public string ContentType { get; } = RedmineConstants.CONTENT_TYPE_APPLICATION_JSON; + + public string Serialize(T entity) where T : class + { + if (entity == null) + { + throw new RedmineSerializationException($"Could not serialize null of type {typeof(T).Name}", nameof(entity)); + } + + EnsureJsonSerializable(); + + if (entity is not IJsonSerializable jsonSerializable) + { + throw new RedmineException($"Entity of type '{typeof(T)}' should implement IJsonSerializable."); + } + + var stringBuilder = new StringBuilder(); + + using var sw = new StringWriter(stringBuilder); + using var writer = new JsonTextWriter(sw); + //writer.Formatting = Formatting.Indented; + writer.DateFormatHandling = DateFormatHandling.IsoDateFormat; + + jsonSerializable.WriteJson(writer); + + var json = stringBuilder.ToString(); + + stringBuilder.Length = 0; + + return json; + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/RedmineSerializerFactory.cs b/src/redmine-net-api/Serialization/RedmineSerializerFactory.cs new file mode 100644 index 00000000..6c71326e --- /dev/null +++ b/src/redmine-net-api/Serialization/RedmineSerializerFactory.cs @@ -0,0 +1,47 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Xml; + +namespace Redmine.Net.Api.Serialization; + +/// +/// Factory for creating RedmineSerializer instances +/// +internal static class RedmineSerializerFactory +{ + /// + /// Creates an instance of an IRedmineSerializer based on the specified serialization type. + /// + /// The type of serialization, either Xml or Json. + /// + /// An instance of a serializer that implements the IRedmineSerializer interface. + /// + /// + /// Thrown when the specified serialization type is not supported. + /// + public static IRedmineSerializer CreateSerializer(SerializationType type) + { + return type switch + { + SerializationType.Xml => new XmlRedmineSerializer(), + SerializationType.Json => new JsonRedmineSerializer(), + _ => throw new NotImplementedException($"No serializer has been implemented for the serialization type: {type}") + }; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/SerializationHelper.cs b/src/redmine-net-api/Serialization/SerializationHelper.cs new file mode 100644 index 00000000..c54ce852 --- /dev/null +++ b/src/redmine-net-api/Serialization/SerializationHelper.cs @@ -0,0 +1,58 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Xml; + +namespace Redmine.Net.Api.Serialization +{ + /// + /// Provides helper methods for serializing user-related data for communication with the Redmine API. + /// + internal static class SerializationHelper + { + /// + /// Serializes the user ID into a format suitable for communication with the Redmine API, + /// based on the specified serializer type. + /// + /// The ID of the user to be serialized. + /// The serializer used to format the user ID (e.g., XML or JSON). + /// A serialized representation of the user ID. + /// + /// Thrown when the provided serializer is not recognized or supported. + /// + public static string SerializeUserId(int userId, IRedmineSerializer redmineSerializer) + { + return redmineSerializer switch + { + XmlRedmineSerializer => $"{userId.ToInvariantString()}", + JsonRedmineSerializer => $"{{\"user_id\":\"{userId.ToInvariantString()}\"}}", + _ => throw new ArgumentOutOfRangeException(nameof(redmineSerializer), redmineSerializer, null) + }; + } + + public static void EnsureDeserializationInputIsNotNullOrWhiteSpace(string input, string paramName, Type type) + { + if (input.IsNullOrWhiteSpace()) + { + throw new RedmineSerializationException($"Could not deserialize null or empty input for type '{type.Name}'.", paramName); + } + } + } +} \ No newline at end of file diff --git a/redmine-net20-api/Types/GroupUser.cs b/src/redmine-net-api/Serialization/SerializationType.cs old mode 100755 new mode 100644 similarity index 65% rename from redmine-net20-api/Types/GroupUser.cs rename to src/redmine-net-api/Serialization/SerializationType.cs index 7d5abf57..3830d3fe --- a/redmine-net20-api/Types/GroupUser.cs +++ b/src/redmine-net-api/Serialization/SerializationType.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2017 Adrian Popescu. + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,23 +14,21 @@ You may obtain a copy of the License at limitations under the License. */ -using System.Xml.Serialization; - -namespace Redmine.Net.Api.Types +namespace Redmine.Net.Api.Serialization { /// - /// + /// Specifies the serialization types supported by the Redmine API. /// - [XmlRoot(RedmineKeys.USER)] - public class GroupUser : IdentifiableName + public enum SerializationType { /// - /// + /// The XML format. + /// + Xml, + + /// + /// The JSON format. /// - /// - public override string ToString () - { - return string.Format ("[GroupUser: {0}]", base.ToString()); - } + Json } } \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/Xml/CacheKeyFactory.cs b/src/redmine-net-api/Serialization/Xml/CacheKeyFactory.cs new file mode 100644 index 00000000..47cef30d --- /dev/null +++ b/src/redmine-net-api/Serialization/Xml/CacheKeyFactory.cs @@ -0,0 +1,88 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Text; +using System.Xml.Serialization; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.Serialization.Xml +{ + /// + /// The CacheKeyFactory extracts a unique signature + /// to identify each instance of an XmlSerializer + /// in the cache. + /// + internal static class CacheKeyFactory + { + + /// + /// Creates a unique signature for the passed + /// in parameter. MakeKey normalizes array content + /// and the content of the XmlAttributeOverrides before + /// creating the key. + /// + /// + /// + /// + /// + /// + /// + public static string Create(Type type, XmlAttributeOverrides overrides, Type[] types, XmlRootAttribute root, string defaultNamespace) + { + var keyBuilder = new StringBuilder(); + keyBuilder.Append(type.FullName); + keyBuilder.Append( "??" ); + keyBuilder.Append(overrides?.GetHashCode().ToInvariantString()); + keyBuilder.Append( "??" ); + keyBuilder.Append(GetTypeArraySignature(types)); + keyBuilder.Append("??"); + keyBuilder.Append(root?.GetHashCode().ToInvariantString()); + keyBuilder.Append("??"); + keyBuilder.Append(defaultNamespace); + + return keyBuilder.ToString(); + } + + /// + /// Creates a signature for the passed in Type array. The order + /// of the elements in the array does not influence the signature. + /// + /// + /// An instance independent signature of the Type array + public static string GetTypeArraySignature(Type[] types) + { + if (null == types || types.Length <= 0) + { + return null; + } + + // to make sure we don't account for the order + // of the types in the array, we create one SortedList + // with the type names, concatenate them and hash that. + var sorter = new string[types.Length]; + for (var index = 0; index < types.Length; index++) + { + Type t = types[index]; + sorter[index] = t.AssemblyQualifiedName; + } + + Array.Sort(sorter); + + return string.Join(":", sorter); + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/Xml/Extensions/XmlReaderExtensions.cs b/src/redmine-net-api/Serialization/Xml/Extensions/XmlReaderExtensions.cs new file mode 100644 index 00000000..3342ed05 --- /dev/null +++ b/src/redmine-net-api/Serialization/Xml/Extensions/XmlReaderExtensions.cs @@ -0,0 +1,299 @@ +ο»Ώ/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Xml; +using System.Xml.Serialization; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.Serialization.Xml.Extensions +{ + /// + /// + public static class XmlReaderExtensions + { + /// + /// Date time format for journals, attachments etc. + /// + private const string INCLUDE_DATE_TIME_FORMAT = "yyyy'-'MM'-'dd HH':'mm':'ss UTC"; + + /// + /// Reads the attribute as int. + /// + /// The reader. + /// Name of the attribute. + /// + public static int ReadAttributeAsInt(this XmlReader reader, string attributeName) + { + var attribute = reader.GetAttribute(attributeName); + + if (attribute.IsNullOrWhiteSpace() || !int.TryParse(attribute, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out var result)) + { + return default; + } + + return result; + } + + /// + /// Reads the attribute as nullable int. + /// + /// The reader. + /// Name of the attribute. + /// + public static int? ReadAttributeAsNullableInt(this XmlReader reader, string attributeName) + { + var attribute = reader.GetAttribute(attributeName); + + if (attribute.IsNullOrWhiteSpace() || !int.TryParse(attribute, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out var result)) + { + return default; + } + + return result; + } + + /// + /// Reads the attribute as boolean. + /// + /// The reader. + /// Name of the attribute. + /// + public static bool ReadAttributeAsBoolean(this XmlReader reader, string attributeName) + { + var attribute = reader.GetAttribute(attributeName); + + if (attribute.IsNullOrWhiteSpace() || !bool.TryParse(attribute, out var result)) + { + return false; + } + + return result; + } + + /// + /// Reads the element content as nullable boolean. + /// + /// The reader. + /// + public static bool? ReadElementContentAsNullableBoolean(this XmlReader reader) + { + var content = reader.ReadElementContentAsString(); + + if (content.IsNullOrWhiteSpace() || !bool.TryParse(content, out var result)) + { + return null; + } + + return result; + } + + /// + /// Reads the element content as nullable date time. + /// + /// The reader. + /// + public static DateTime? ReadElementContentAsNullableDateTime(this XmlReader reader) + { + var content = reader.ReadElementContentAsString(); + + if (!content.IsNullOrWhiteSpace() && DateTime.TryParse(content, out var result)) + { + return result; + } + + if (!DateTime.TryParseExact(content, INCLUDE_DATE_TIME_FORMAT, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) + { + return null; + } + + return result; + } + + /// + /// Reads the element content as nullable float. + /// + /// The reader. + /// + public static float? ReadElementContentAsNullableFloat(this XmlReader reader) + { + var content = reader.ReadElementContentAsString(); + + if (content.IsNullOrWhiteSpace() || !float.TryParse(content, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out var result)) + { + return null; + } + + return result; + } + + /// + /// Reads the element content as nullable int. + /// + /// The reader. + /// + public static int? ReadElementContentAsNullableInt(this XmlReader reader) + { + var content = reader.ReadElementContentAsString(); + + if (content.IsNullOrWhiteSpace() || !int.TryParse(content, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out var result)) + { + return null; + } + + return result; + } + + /// + /// Reads the element content as nullable decimal. + /// + /// The reader. + /// + public static decimal? ReadElementContentAsNullableDecimal(this XmlReader reader) + { + var content = reader.ReadElementContentAsString(); + + if (content.IsNullOrWhiteSpace() || !decimal.TryParse(content, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out var result)) + { + return null; + } + + return result; + } + + /// + /// Reads the element content as collection. + /// + /// + /// The reader. + /// + public static List ReadElementContentAsCollection(this XmlReader reader) where T : class + { + List result = null; + var serializer = new XmlSerializer(typeof(T)); + var outerXml = reader.ReadOuterXml(); + + if (string.IsNullOrEmpty(outerXml)) + { + return null; + } + + using (var stringReader = new StringReader(outerXml)) + { + using (var xmlTextReader = XmlTextReaderBuilder.Create(stringReader)) + { + xmlTextReader.ReadStartElement(); + while (!xmlTextReader.EOF) + { + if (xmlTextReader.NodeType == XmlNodeType.EndElement) + { + xmlTextReader.ReadEndElement(); + continue; + } + + T entity; + + if (xmlTextReader.IsEmptyElement && xmlTextReader.HasAttributes) + { + entity = serializer.Deserialize(xmlTextReader) as T; + } + else + { + if (xmlTextReader.NodeType != XmlNodeType.Element) + { + xmlTextReader.Read(); + continue; + } + + var subTree = xmlTextReader.ReadSubtree(); + entity = serializer.Deserialize(subTree) as T; + } + + if (entity != null) + { + result ??= new List(); + + result.Add(entity); + } + + if (!xmlTextReader.IsEmptyElement) + { + xmlTextReader.Read(); + } + } + } + } + return result; + } + + /// + /// Reads the element content as enumerable. + /// + /// + /// The reader. + /// + public static IEnumerable ReadElementContentAsEnumerable(this XmlReader reader) where T : class + { + var serializer = new XmlSerializer(typeof(T)); + var outerXml = reader.ReadOuterXml(); + + if (string.IsNullOrEmpty(outerXml)) + { + yield return null; + } + + using (var stringReader = new StringReader(outerXml)) + { + using (var xmlTextReader = XmlTextReaderBuilder.Create(stringReader)) + { + xmlTextReader.ReadStartElement(); + while (!xmlTextReader.EOF) + { + if (xmlTextReader.NodeType == XmlNodeType.EndElement) + { + xmlTextReader.ReadEndElement(); + continue; + } + + T entity; + + if (xmlTextReader.IsEmptyElement && xmlTextReader.HasAttributes) + { + entity = serializer.Deserialize(xmlTextReader) as T; + } + else + { + var subTree = xmlTextReader.ReadSubtree(); + entity = serializer.Deserialize(subTree) as T; + } + if (entity != null) + { + yield return entity; + } + + if (!xmlTextReader.IsEmptyElement) + { + xmlTextReader.Read(); + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/Xml/Extensions/XmlWriterExtensions.cs b/src/redmine-net-api/Serialization/Xml/Extensions/XmlWriterExtensions.cs new file mode 100644 index 00000000..b641af40 --- /dev/null +++ b/src/redmine-net-api/Serialization/Xml/Extensions/XmlWriterExtensions.cs @@ -0,0 +1,346 @@ +ο»Ώ/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Xml; +using System.Xml.Serialization; +using Redmine.Net.Api.Common; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.Serialization.Xml.Extensions +{ + /// + /// + /// + public static partial class XmlExtensions + { + +#if !(NET20 || NET40 || NET45 || NET451 || NET452) + private static readonly Type[] EmptyTypeArray = Array.Empty(); +#else + private static readonly Type[] EmptyTypeArray = new Type[0]; +#endif + private static readonly XmlAttributeOverrides XmlAttributeOverrides = new XmlAttributeOverrides(); + + /// + /// Writes the id if not null. + /// + /// The writer. + /// + /// + public static void WriteIdIfNotNull(this XmlWriter writer, string elementName, IdentifiableName identifiableName) + { + if (identifiableName != null) + { + writer.WriteElementString(elementName, identifiableName.Id.ToInvariantString()); + } + } + + /// + /// Writes the array. + /// + /// The writer. + /// The collection. + /// Name of the element. + public static void WriteArray(this XmlWriter writer, string elementName, IEnumerable collection) + { + if (collection == null) + { + return; + } + + writer.WriteStartElement(elementName); + writer.WriteAttributeString("type", "array"); + + foreach (var item in collection) + { + new XmlSerializer(item.GetType()).Serialize(writer, item); + } + + writer.WriteEndElement(); + } + + /// + /// Writes the array. + /// + /// The writer. + /// The collection. + /// Name of the element. + public static void WriteArray(this XmlWriter writer, string elementName, IEnumerable collection) + { + if (collection == null) + { + return; + } + + writer.WriteStartElement(elementName); + writer.WriteAttributeString("type", "array"); + + var serializer = new XmlSerializer(typeof(T)); + + foreach (var item in collection) + { + serializer.Serialize(writer, item); + } + + writer.WriteEndElement(); + } + + /// + /// Writes the array ids. + /// + /// The writer. + /// The collection. + /// Name of the element. + /// The type. + /// The f. + public static void WriteArrayIds(this XmlWriter writer, string elementName, IEnumerable collection, Type type, Func f) + { + if (collection == null || f == null) + { + return; + } + + writer.WriteStartElement(elementName); + writer.WriteAttributeString("type", "array"); + + var serializer = new XmlSerializer(type); + + foreach (var item in collection) + { + serializer.Serialize(writer, f.Invoke(item)); + } + + writer.WriteEndElement(); + } + + /// + /// Writes the array. + /// + /// The writer. + /// The collection. + /// Name of the element. + /// The type. + /// The root. + /// The default namespace. + public static void WriteArray(this XmlWriter writer, string elementName, IEnumerable collection, Type type, string root, string defaultNamespace = null) + { + if (collection == null) + { + return; + } + + writer.WriteStartElement(elementName); + writer.WriteAttributeString("type", "array"); + + var rootAttribute = new XmlRootAttribute(root); + + var serializer = new XmlSerializer(type, XmlAttributeOverrides, EmptyTypeArray, rootAttribute, + defaultNamespace); + + foreach (var item in collection) + { + serializer.Serialize(writer, item); + } + + writer.WriteEndElement(); + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static void WriteArray(this XmlWriter writer, string elementName, IEnumerable collection, string root, string defaultNamespace = null) + { + if (collection == null) + { + return; + } + + var type = typeof(T); + writer.WriteStartElement(elementName); + writer.WriteAttributeString("type", "array"); + + var rootAttribute = new XmlRootAttribute(root); + + var serializer = new XmlSerializer(type, XmlAttributeOverrides, EmptyTypeArray, rootAttribute, + defaultNamespace); + + foreach (var item in collection) + { + serializer.Serialize(writer, item); + } + + writer.WriteEndElement(); + } + + /// + /// Writes the list elements. + /// + /// The XML writer. + /// The collection. + /// Name of the element. + public static void WriteListElements(this XmlWriter xmlWriter, string elementName, IEnumerable collection) + { + if (collection == null) + { + return; + } + + foreach (var item in collection) + { + xmlWriter.WriteElementString(elementName, item.Value); + } + } + + /// + /// + /// + /// + /// + /// + public static void WriteRepeatableElement(this XmlWriter xmlWriter, string elementName, IEnumerable collection) + { + if (collection == null) + { + return; + } + + foreach (var item in collection) + { + xmlWriter.WriteElementString(elementName, item.Value); + } + } + + /// + /// + /// + /// + /// + /// + /// + public static void WriteArrayStringElement(this XmlWriter writer, string elementName, IEnumerable collection, Func f) + { + if (collection == null) + { + return; + } + + writer.WriteStartElement(elementName); + writer.WriteAttributeString("type", "array"); + + foreach (var item in collection) + { + writer.WriteElementString(elementName, f.Invoke(item)); + } + + writer.WriteEndElement(); + } + + /// + /// + /// + /// + /// + /// + public static void WriteIdOrEmpty(this XmlWriter writer, string elementName, IdentifiableName ident) + { + writer.WriteElementString(elementName, ident != null ? ident.Id.ToInvariantString() : string.Empty); + } + + /// + /// Writes if not default or null. + /// + /// + /// The writer. + /// The value. + /// The tag. + public static void WriteIfNotDefaultOrNull(this XmlWriter writer, string elementName, T value) + { + if (EqualityComparer.Default.Equals(value, default)) + { + return; + } + + if (value is bool) + { + writer.WriteElementString(elementName, value.ToString().ToLowerInv()); + } + else + { + writer.WriteElementString(elementName, value.ToString()); + } + } + + /// + /// Writes the boolean value + /// + /// The writer. + /// The value. + /// The tag. + public static void WriteBoolean(this XmlWriter writer, string elementName, bool value) + { + writer.WriteElementString(elementName, value.ToInvariantString()); + } + + /// + /// Writes string empty if T has default value or null. + /// + /// + /// The writer. + /// The value. + /// The tag. + public static void WriteValueOrEmpty(this XmlWriter writer, string elementName, T? val) where T : struct + { + if (!val.HasValue || EqualityComparer.Default.Equals(val.Value, default)) + { + writer.WriteElementString(elementName, string.Empty); + } + else + { + writer.WriteElementString(elementName, val.Value.ToInvariantString()); + } + } + + /// + /// Writes the date or empty. + /// + /// The writer. + /// The tag. + /// The value. + /// + public static void WriteDateOrEmpty(this XmlWriter writer, string elementName, DateTime? val, string dateTimeFormat = "yyyy-MM-dd") + { + if (!val.HasValue || val.Value.Equals(default)) + { + writer.WriteElementString(elementName, string.Empty); + } + else + { + writer.WriteElementString(elementName, val.Value.ToString(dateTimeFormat, CultureInfo.InvariantCulture)); + } + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/Xml/IXmlSerializerCache.cs b/src/redmine-net-api/Serialization/Xml/IXmlSerializerCache.cs new file mode 100644 index 00000000..a11e2d33 --- /dev/null +++ b/src/redmine-net-api/Serialization/Xml/IXmlSerializerCache.cs @@ -0,0 +1,34 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Xml.Serialization; + +namespace Redmine.Net.Api.Serialization.Xml +{ + internal interface IXmlSerializerCache + { + XmlSerializer GetSerializer(Type type, string defaultNamespace); + + XmlSerializer GetSerializer(Type type, XmlRootAttribute root); + + XmlSerializer GetSerializer(Type type, XmlAttributeOverrides overrides); + + XmlSerializer GetSerializer(Type type, Type[] types); + + XmlSerializer GetSerializer(Type type, XmlAttributeOverrides overrides, Type[] types, XmlRootAttribute root, string defaultNamespace); + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs b/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs new file mode 100644 index 00000000..266c14b0 --- /dev/null +++ b/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs @@ -0,0 +1,197 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.IO; +using System.Xml; +using System.Xml.Serialization; +using Redmine.Net.Api.Common; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Serialization.Xml +{ + internal sealed class XmlRedmineSerializer : IRedmineSerializer + { + private static void EnsureXmlSerializable() + { + if (!typeof(IXmlSerializable).IsAssignableFrom(typeof(T))) + { + throw new RedmineException($"Entity of type '{typeof(T)}' should implement ${nameof(IXmlSerializable)}."); + } + } + public XmlRedmineSerializer() : this(new XmlWriterSettings + { + OmitXmlDeclaration = true + }) { } + + public XmlRedmineSerializer(XmlWriterSettings xmlWriterSettings) + { + this._xmlWriterSettings = xmlWriterSettings; + } + + private readonly XmlWriterSettings _xmlWriterSettings; + + public T Deserialize(string response) where T : new() + { + try + { + return XmlDeserializeEntity(response); + } + catch (Exception ex) + { + throw new RedmineException(ex.GetBaseException().Message, ex); + } + } + + public PagedResults DeserializeToPagedResults(string response) where T : class, new() + { + try + { + return XmlDeserializeList(response, false); + } + catch (Exception ex) + { + throw new RedmineException(ex.GetBaseException().Message, ex); + } + } + +#pragma warning disable CA1822 + public int Count(string xmlResponse) where T : class, new() + { + try + { + var pagedResults = XmlDeserializeList(xmlResponse, true); + return pagedResults.TotalItems; + } + catch (Exception ex) + { + throw new RedmineException(ex.GetBaseException().Message, ex); + } + } +#pragma warning restore CA1822 + + public string Format => RedmineConstants.XML; + + public string ContentType { get; } = RedmineConstants.CONTENT_TYPE_APPLICATION_XML; + + public string Serialize(T entity) where T : class + { + try + { + return ToXML(entity); + } + catch (Exception ex) + { + throw new RedmineException(ex.GetBaseException().Message, ex); + } + } + + /// + /// XMLs the deserialize list. + /// + /// + /// The response. + /// + /// + private static PagedResults XmlDeserializeList(string xmlResponse, bool onlyCount) where T : class, new() + { + SerializationHelper.EnsureDeserializationInputIsNotNullOrWhiteSpace(xmlResponse, nameof(xmlResponse), typeof(T)); + + using var stringReader = new StringReader(xmlResponse); + using var xmlReader = XmlTextReaderBuilder.Create(stringReader); + while (xmlReader.NodeType == XmlNodeType.None || xmlReader.NodeType == XmlNodeType.XmlDeclaration) + { + xmlReader.Read(); + } + + var totalItems = xmlReader.ReadAttributeAsInt(RedmineKeys.TOTAL_COUNT); + + if (onlyCount) + { + return new PagedResults(null, totalItems, 0, 0); + } + + var offset = xmlReader.ReadAttributeAsInt(RedmineKeys.OFFSET); + var limit = xmlReader.ReadAttributeAsInt(RedmineKeys.LIMIT); + var result = xmlReader.ReadElementContentAsCollection(); + + if (totalItems == 0 && result?.Count > 0) + { + totalItems = result.Count; + } + + return new PagedResults(result, totalItems, offset, limit); + } + + /// + /// Serializes the specified System.Object and writes the XML document to a string. + /// + /// The type of objects to serialize. + /// The object to serialize. + /// + /// The System.String that contains the XML document. + /// + /// + // ReSharper disable once InconsistentNaming + private string ToXML(T entity) where T : class + { + if (entity == null) + { + throw new ArgumentNullException(nameof(entity), $"Could not serialize null of type {typeof(T).Name}"); + } + + using var stringWriter = new StringWriter(); + using var xmlWriter = XmlWriter.Create(stringWriter, _xmlWriterSettings); + var serializer = new XmlSerializer(typeof(T)); + + serializer.Serialize(xmlWriter, entity); + + return stringWriter.ToString(); + } + + /// + /// Deserializes the XML document contained by the specific System.String. + /// + /// The type of objects to deserialize. + /// The System.String that contains the XML document to deserialize. + /// + /// The T object being deserialized. + /// + /// + /// An error occurred during deserialization. The original exception is available + /// using the System.Exception.InnerException property. + /// + // ReSharper disable once InconsistentNaming + private static TOut XmlDeserializeEntity(string xml) where TOut : new() + { + SerializationHelper.EnsureDeserializationInputIsNotNullOrWhiteSpace(xml, nameof(xml), typeof(TOut)); + + using var textReader = new StringReader(xml); + using var xmlReader = XmlTextReaderBuilder.Create(textReader); + var serializer = new XmlSerializer(typeof(TOut)); + + var entity = serializer.Deserialize(xmlReader); + + if (entity is TOut t) + { + return t; + } + + return default; + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/Xml/XmlSerializerCache.cs b/src/redmine-net-api/Serialization/Xml/XmlSerializerCache.cs new file mode 100644 index 00000000..fcd977ba --- /dev/null +++ b/src/redmine-net-api/Serialization/Xml/XmlSerializerCache.cs @@ -0,0 +1,148 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Xml.Serialization; + +namespace Redmine.Net.Api.Serialization.Xml +{ + /// + /// + /// + internal sealed class XmlSerializerCache : IXmlSerializerCache + { + #if !(NET20 || NET40 || NET45 || NET451 || NET452) + private static readonly Type[] EmptyTypes = Array.Empty(); + #else + private static readonly Type[] EmptyTypes = new Type[0]; + #endif + + public static XmlSerializerCache Instance { get; } = new XmlSerializerCache(); + + private readonly Dictionary serializers; + + private readonly object syncRoot; + + private XmlSerializerCache() + { + syncRoot = new object(); + serializers = new Dictionary(); + } + + /// + /// Get an XmlSerializer instance for the + /// specified parameters. The method will check if + /// any any previously cached instances are compatible + /// with the parameters before constructing a new + /// XmlSerializer instance. + /// + /// + /// + /// + public XmlSerializer GetSerializer(Type type, string defaultNamespace) + { + return GetSerializer(type, null, EmptyTypes, null, defaultNamespace); + } + + /// + /// Get an XmlSerializer instance for the + /// specified parameters. The method will check if + /// any any previously cached instances are compatible + /// with the parameters before constructing a new + /// XmlSerializer instance. + /// + /// + /// + /// + public XmlSerializer GetSerializer(Type type, XmlRootAttribute root) + { + return GetSerializer(type, null, EmptyTypes, root, null); + } + + /// + /// Get an XmlSerializer instance for the + /// specified parameters. The method will check if + /// any any previously cached instances are compatible + /// with the parameters before constructing a new + /// XmlSerializer instance. + /// + /// + /// + /// + public XmlSerializer GetSerializer(Type type, XmlAttributeOverrides overrides) + { + return GetSerializer(type, overrides, EmptyTypes, null, null); + } + + /// + /// Get an XmlSerializer instance for the + /// specified parameters. The method will check if + /// any any previously cached instances are compatible + /// with the parameters before constructing a new + /// XmlSerializer instance. + /// + /// + /// + /// + public XmlSerializer GetSerializer(Type type, Type[] types) + { + return GetSerializer(type, null, types, null, null); + } + + /// + /// Get an XmlSerializer instance for the + /// specified parameters. The method will check if + /// any any previously cached instances are compatible + /// with the parameters before constructing a new + /// XmlSerializer instance. + /// + /// + /// + /// + /// + /// + /// + public XmlSerializer GetSerializer(Type type, XmlAttributeOverrides overrides, Type[] types, XmlRootAttribute root, string defaultNamespace) + { + var key = CacheKeyFactory.Create(type, overrides, types, root, defaultNamespace); + + XmlSerializer serializer = null; + lock (syncRoot) + { + if (serializers.ContainsKey(key) == false) + { + lock (syncRoot) + { + if (serializers.ContainsKey(key) == false) + { + serializer = new XmlSerializer(type, overrides, types, root, defaultNamespace); + serializers.Add(key, serializer); + } + } + } + else + { + serializer = serializers[key]; + } + + Debug.Assert(serializer != null); + return serializer; + } + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/Xml/XmlTextReaderBuilder.cs b/src/redmine-net-api/Serialization/Xml/XmlTextReaderBuilder.cs new file mode 100644 index 00000000..6a01e8ab --- /dev/null +++ b/src/redmine-net-api/Serialization/Xml/XmlTextReaderBuilder.cs @@ -0,0 +1,95 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System.IO; +using System.Xml; + +namespace Redmine.Net.Api.Serialization.Xml +{ + /// + /// + /// + public static class XmlTextReaderBuilder + { +#if NET20 + private static readonly XmlReaderSettings XmlReaderSettings = new XmlReaderSettings() + { + ProhibitDtd = true, + XmlResolver = null, + IgnoreComments = true, + IgnoreWhitespace = true, + }; + + /// + /// + /// + /// + /// + public static XmlReader Create(StringReader stringReader) + { + return XmlReader.Create(stringReader, XmlReaderSettings); + + } + + /// + /// + /// + /// + /// + public static XmlReader Create(string xml) + { + var stringReader = new StringReader(xml); + { + return XmlReader.Create(stringReader, XmlReaderSettings); + } + } +#else + /// + /// + /// + /// + /// + public static XmlTextReader Create(StringReader stringReader) + { + return new XmlTextReader(stringReader) + { + DtdProcessing = DtdProcessing.Prohibit, + XmlResolver = null, + WhitespaceHandling = WhitespaceHandling.None + }; + } + + + /// + /// + /// + /// + /// + public static XmlTextReader Create(string xml) + { + var stringReader = new StringReader(xml); + { + return new XmlTextReader(stringReader) + { + DtdProcessing = DtdProcessing.Prohibit, + XmlResolver = null, + WhitespaceHandling = WhitespaceHandling.None + }; + } + } +#endif + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/Attachment.cs b/src/redmine-net-api/Types/Attachment.cs new file mode 100644 index 00000000..dc36465c --- /dev/null +++ b/src/redmine-net-api/Types/Attachment.cs @@ -0,0 +1,294 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// Availability 1.3 + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.ATTACHMENT)] + public sealed class Attachment : + Identifiable + , ICloneable + { + #region Properties + /// + /// Gets or sets the name of the file. + /// + /// The name of the file. + public string FileName { get; set; } + + /// + /// Gets the size of the file. + /// + /// The size of the file. + public int FileSize { get; internal set; } + + /// + /// Gets the type of the content. + /// + /// The type of the content. + public string ContentType { get; internal set; } + + /// + /// Gets or sets the description. + /// + /// The description. + public string Description { get; set; } + + /// + /// Gets the content URL. + /// + /// The content URL. + public string ContentUrl { get; internal set; } + + /// + /// Gets the author. + /// + /// The author. + public IdentifiableName Author { get; internal set; } + + /// + /// Gets the created on. + /// + /// The created on. + public DateTime? CreatedOn { get; internal set; } + + /// + /// Gets the thumbnail url. + /// + public string ThumbnailUrl { get; internal set; } + #endregion + + #region Implementation of IXmlSerializable + + /// + /// + /// + /// + public override void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; + case RedmineKeys.CONTENT_TYPE: ContentType = reader.ReadElementContentAsString(); break; + case RedmineKeys.CONTENT_URL: ContentUrl = reader.ReadElementContentAsString(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; + case RedmineKeys.FILE_NAME: FileName = reader.ReadElementContentAsString(); break; + case RedmineKeys.FILE_SIZE: FileSize = reader.ReadElementContentAsInt(); break; + case RedmineKeys.THUMBNAIL_URL: ThumbnailUrl = reader.ReadElementContentAsString(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteXml(XmlWriter writer) + { + writer.WriteElementString(RedmineKeys.FILE_NAME, FileName); + writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); + } + + #endregion + + #region Implementation of IJsonSerializable + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; + case RedmineKeys.CONTENT_TYPE: ContentType = reader.ReadAsString(); break; + case RedmineKeys.CONTENT_URL: ContentUrl = reader.ReadAsString(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadAsString(); break; + case RedmineKeys.FILE_NAME: FileName = reader.ReadAsString(); break; + case RedmineKeys.FILE_SIZE: FileSize = reader.ReadAsInt(); break; + case RedmineKeys.THUMBNAIL_URL: ThumbnailUrl = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + + public override void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.ATTACHMENT)) + { + writer.WriteProperty(RedmineKeys.FILE_NAME, FileName); + writer.WriteProperty(RedmineKeys.DESCRIPTION, Description); + } + } + #endregion + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public override bool Equals(Attachment other) + { + if (other == null) return false; + return base.Equals(other) + && string.Equals(FileName, other.FileName, StringComparison.Ordinal) + && string.Equals(ContentType, other.ContentType, StringComparison.Ordinal) + && string.Equals(Description, other.Description, StringComparison.Ordinal) + && string.Equals(ContentUrl, other.ContentUrl, StringComparison.Ordinal) + && string.Equals(ThumbnailUrl, other.ThumbnailUrl, StringComparison.Ordinal) + && Author == other.Author + && FileSize == other.FileSize + && CreatedOn == other.CreatedOn; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as Attachment); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(FileName, hashCode); + hashCode = HashCodeHelper.GetHashCode(FileSize, hashCode); + hashCode = HashCodeHelper.GetHashCode(ContentType, hashCode); + hashCode = HashCodeHelper.GetHashCode(Description, hashCode); + hashCode = HashCodeHelper.GetHashCode(ContentUrl, hashCode); + hashCode = HashCodeHelper.GetHashCode(ThumbnailUrl, hashCode); + hashCode = HashCodeHelper.GetHashCode(Author, hashCode); + hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Attachment left, Attachment right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Attachment left, Attachment right) + { + return !Equals(left, right); + } + #endregion + + private string DebuggerDisplay =>$"[Attachment: Id={Id.ToInvariantString()}, FileName={FileName}, FileSize={FileSize.ToInvariantString()}]"; + + /// + /// + /// + /// + public new Attachment Clone(bool resetId) + { + if (resetId) + { + return new Attachment + { + FileName = FileName, + FileSize = FileSize, + ContentType = ContentType, + Description = Description, + ContentUrl = ContentUrl, + ThumbnailUrl = ThumbnailUrl, + Author = Author?.Clone(false), + CreatedOn = CreatedOn + }; + } + + return new Attachment + { + Id = Id, + FileName = FileName, + FileSize = FileSize, + ContentType = ContentType, + Description = Description, + ContentUrl = ContentUrl, + ThumbnailUrl = ThumbnailUrl, + Author = Author?.Clone(true), + CreatedOn = CreatedOn + }; + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/Attachments.cs b/src/redmine-net-api/Types/Attachments.cs new file mode 100644 index 00000000..15b5e68f --- /dev/null +++ b/src/redmine-net-api/Types/Attachments.cs @@ -0,0 +1,53 @@ +ο»Ώ/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System.Collections.Generic; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Serialization.Json; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + internal sealed class Attachments : Dictionary, IJsonSerializable + { + /// + /// + /// + /// + public void ReadJson(JsonReader reader) { } + + /// + /// + /// + /// + public void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.ATTACHMENTS)) + { + writer.WriteStartArray(); + foreach (var item in this) + { + writer.WritePropertyName(item.Key.ToInvariantString()); + item.Value.WriteJson(writer); + } + writer.WriteEndArray(); + } + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/ChangeSet.cs b/src/redmine-net-api/Types/ChangeSet.cs new file mode 100644 index 00000000..45e4ff1e --- /dev/null +++ b/src/redmine-net-api/Types/ChangeSet.cs @@ -0,0 +1,235 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Diagnostics; +using System.Globalization; +using System.Xml; +using System.Xml.Schema; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.CHANGE_SET)] + public sealed class ChangeSet : IXmlSerializable, IJsonSerializable, IEquatable + ,ICloneable + { + #region Properties + /// + /// + /// + public string Revision { get; internal set; } + + /// + /// + /// + public IdentifiableName User { get; internal set; } + + /// + /// + /// + public string Comments { get; internal set; } + + /// + /// + /// + public DateTime? CommittedOn { get; internal set; } + #endregion + + #region Implementation of IXmlSerializable + /// + /// + /// + /// + public XmlSchema GetSchema() { return null; } + + /// + /// + /// + /// + public void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + Revision = reader.GetAttribute(RedmineKeys.REVISION); + + switch (reader.Name) + { + case RedmineKeys.COMMENTS: Comments = reader.ReadElementContentAsString(); break; + + case RedmineKeys.COMMITTED_ON: CommittedOn = reader.ReadElementContentAsNullableDateTime(); break; + + case RedmineKeys.USER: User = new IdentifiableName(reader); break; + + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public void WriteXml(XmlWriter writer) { } + #endregion + + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.COMMENTS: Comments = reader.ReadAsString(); break; + + case RedmineKeys.COMMITTED_ON: CommittedOn = reader.ReadAsDateTime(); break; + + case RedmineKeys.REVISION: Revision = reader.ReadAsString(); break; + + case RedmineKeys.USER: User = new IdentifiableName(reader); break; + + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public void WriteJson(JsonWriter writer) { } + #endregion + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public bool Equals(ChangeSet other) + { + if (other == null) return false; + + return Revision == other.Revision + && User == other.User + && string.Equals(Comments, other.Comments, StringComparison.Ordinal) + && CommittedOn == other.CommittedOn; + } + + /// + /// + /// + /// + /// + /// + public ChangeSet Clone(bool resetId) + { + return new ChangeSet() + { + User = User, + Comments = Comments, + Revision = Revision, + CommittedOn = CommittedOn, + }; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as ChangeSet); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Revision, hashCode); + hashCode = HashCodeHelper.GetHashCode(User, hashCode); + hashCode = HashCodeHelper.GetHashCode(Comments, hashCode); + hashCode = HashCodeHelper.GetHashCode(CommittedOn, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(ChangeSet left, ChangeSet right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(ChangeSet left, ChangeSet right) + { + return !Equals(left, right); + } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $" ChangeSet: Revision={Revision}, CommittedOn={CommittedOn?.ToString("u", CultureInfo.InvariantCulture)}]"; + + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/CustomField.cs b/src/redmine-net-api/Types/CustomField.cs new file mode 100644 index 00000000..6aeea0e8 --- /dev/null +++ b/src/redmine-net-api/Types/CustomField.cs @@ -0,0 +1,293 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.CUSTOM_FIELD)] + public sealed class CustomField : IdentifiableName, IEquatable + { + #region Properties + /// + /// + /// + public string CustomizedType { get; internal set; } + + /// + /// Added in Redmine 5.1.0 version + /// + public string Description { get; internal set; } + + /// + /// + /// + public string FieldFormat { get; internal set; } + + /// + /// + /// + public string Regexp { get; internal set; } + + /// + /// + /// + public int? MinLength { get; internal set; } + + /// + /// + /// + public int? MaxLength { get; internal set; } + + /// + /// + /// + public bool IsRequired { get; internal set; } + + /// + /// + /// + public bool IsFilter { get; internal set; } + + /// + /// + /// + public bool Searchable { get; internal set; } + + /// + /// + /// + public bool Multiple { get; internal set; } + + /// + /// + /// + public string DefaultValue { get; internal set; } + + /// + /// + /// + public bool Visible { get; internal set; } + + /// + /// + /// + public List PossibleValues { get; internal set; } + + /// + /// + /// + public List Trackers { get; internal set; } + + /// + /// + /// + public List Roles { get; internal set; } + #endregion + + #region Implementation of IXmlSerializable + /// + /// + /// + /// + public override void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.CUSTOMIZED_TYPE: CustomizedType = reader.ReadElementContentAsString(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; + case RedmineKeys.DEFAULT_VALUE: DefaultValue = reader.ReadElementContentAsString(); break; + case RedmineKeys.FIELD_FORMAT: FieldFormat = reader.ReadElementContentAsString(); break; + case RedmineKeys.IS_FILTER: IsFilter = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.IS_REQUIRED: IsRequired = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.MAX_LENGTH: MaxLength = reader.ReadElementContentAsNullableInt(); break; + case RedmineKeys.MIN_LENGTH: MinLength = reader.ReadElementContentAsNullableInt(); break; + case RedmineKeys.MULTIPLE: Multiple = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; + case RedmineKeys.POSSIBLE_VALUES: PossibleValues = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.REGEXP: Regexp = reader.ReadElementContentAsString(); break; + case RedmineKeys.ROLES: Roles = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.SEARCHABLE: Searchable = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.TRACKERS: Trackers = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.VISIBLE: Visible = reader.ReadElementContentAsBoolean(); break; + default: reader.Read(); break; + } + } + } + + #endregion + + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.CUSTOMIZED_TYPE: CustomizedType = reader.ReadAsString(); break; + case RedmineKeys.DEFAULT_VALUE: DefaultValue = reader.ReadAsString(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadAsString(); break; + case RedmineKeys.FIELD_FORMAT: FieldFormat = reader.ReadAsString(); break; + case RedmineKeys.IS_FILTER: IsFilter = reader.ReadAsBool(); break; + case RedmineKeys.IS_REQUIRED: IsRequired = reader.ReadAsBool(); break; + case RedmineKeys.MAX_LENGTH: MaxLength = reader.ReadAsInt32(); break; + case RedmineKeys.MIN_LENGTH: MinLength = reader.ReadAsInt32(); break; + case RedmineKeys.MULTIPLE: Multiple = reader.ReadAsBool(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.POSSIBLE_VALUES: PossibleValues = reader.ReadAsCollection(); break; + case RedmineKeys.REGEXP: Regexp = reader.ReadAsString(); break; + case RedmineKeys.ROLES: Roles = reader.ReadAsCollection(); break; + case RedmineKeys.SEARCHABLE: Searchable = reader.ReadAsBool(); break; + case RedmineKeys.TRACKERS: Trackers = reader.ReadAsCollection(); break; + case RedmineKeys.VISIBLE: Visible = reader.ReadAsBool(); break; + default: reader.Read(); break; + } + } + } + + #endregion + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public bool Equals(CustomField other) + { + if (other == null) return false; + + var result = base.Equals(other) + && string.Equals(CustomizedType, other.CustomizedType, StringComparison.Ordinal) + && string.Equals(Description, other.Description, StringComparison.Ordinal) + && string.Equals(FieldFormat, other.FieldFormat, StringComparison.Ordinal) + && string.Equals(Regexp, other.Regexp, StringComparison.Ordinal) + && string.Equals(DefaultValue, other.DefaultValue, StringComparison.Ordinal) + && MinLength == other.MinLength + && MaxLength == other.MaxLength + && IsRequired == other.IsRequired + && IsFilter == other.IsFilter + && Searchable == other.Searchable + && Multiple == other.Multiple + && Visible == other.Visible + && (PossibleValues?.Equals(other.PossibleValues) ?? other.PossibleValues == null) + && (Trackers?.Equals(other.Trackers) ?? other.Trackers == null) + && (Roles?.Equals(other.Roles) ?? other.Roles == null); + return result; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as CustomField); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(CustomizedType, hashCode); + hashCode = HashCodeHelper.GetHashCode(Description, hashCode); + hashCode = HashCodeHelper.GetHashCode(FieldFormat, hashCode); + hashCode = HashCodeHelper.GetHashCode(Regexp, hashCode); + hashCode = HashCodeHelper.GetHashCode(MinLength, hashCode); + hashCode = HashCodeHelper.GetHashCode(MaxLength, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsRequired, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsFilter, hashCode); + hashCode = HashCodeHelper.GetHashCode(Searchable, hashCode); + hashCode = HashCodeHelper.GetHashCode(Multiple, hashCode); + hashCode = HashCodeHelper.GetHashCode(DefaultValue, hashCode); + hashCode = HashCodeHelper.GetHashCode(Visible, hashCode); + hashCode = HashCodeHelper.GetHashCode(PossibleValues, hashCode); + hashCode = HashCodeHelper.GetHashCode(Trackers, hashCode); + hashCode = HashCodeHelper.GetHashCode(Roles, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(CustomField left, CustomField right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(CustomField left, CustomField right) + { + return !Equals(left, right); + } + #endregion + + private string DebuggerDisplay => $"[CustomField: Id={Id.ToInvariantString()}, Name={Name}]"; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs new file mode 100644 index 00000000..2e3f6318 --- /dev/null +++ b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs @@ -0,0 +1,198 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Diagnostics; +using System.Xml; +using System.Xml.Schema; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.POSSIBLE_VALUE)] + public sealed class CustomFieldPossibleValue : IXmlSerializable, IJsonSerializable, IEquatable + { + #region Properties + /// + /// + /// + public string Value { get; internal set; } + + /// + /// + /// + public string Label { get; internal set; } + #endregion + + #region Implementation of IXmlSerializable + + /// + /// + /// + /// + public XmlSchema GetSchema() + { + return null; + } + + /// + /// + /// + /// + public void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.LABEL: Label = reader.ReadElementContentAsString(); break; + case RedmineKeys.VALUE: Value = reader.ReadElementContentAsString(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public void WriteXml(XmlWriter writer) { } + + #endregion + + #region Implementation of IJsonSerialization + + /// + /// + /// + /// + public void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.LABEL: Label = reader.ReadAsString(); break; + case RedmineKeys.VALUE: Value = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public void WriteJson(JsonWriter writer) { } + #endregion + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public bool Equals(CustomFieldPossibleValue other) + { + if (other == null) return false; + var result = string.Equals(Value, other.Value, StringComparison.Ordinal) + && string.Equals(Label, other.Label, StringComparison.Ordinal); + return result; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as CustomFieldPossibleValue); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + unchecked + { + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Value, hashCode); + hashCode = HashCodeHelper.GetHashCode(Label, hashCode); + return hashCode; + } + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(CustomFieldPossibleValue left, CustomFieldPossibleValue right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(CustomFieldPossibleValue left, CustomFieldPossibleValue right) + { + return !Equals(left, right); + } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[CustomFieldPossibleValue: Label:{Label}, Value:{Value}]"; + + } +} \ No newline at end of file diff --git a/redmine-net20-api/Types/Watcher.cs b/src/redmine-net-api/Types/CustomFieldRole.cs old mode 100755 new mode 100644 similarity index 54% rename from redmine-net20-api/Types/Watcher.cs rename to src/redmine-net-api/Types/CustomFieldRole.cs index 400aeaa5..7b1b20a9 --- a/redmine-net20-api/Types/Watcher.cs +++ b/src/redmine-net-api/Types/CustomFieldRole.cs @@ -1,5 +1,5 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. +/* + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,48 +14,35 @@ You may obtain a copy of the License at limitations under the License. */ -using System; +using System.Diagnostics; using System.Xml.Serialization; +using Redmine.Net.Api.Extensions; namespace Redmine.Net.Api.Types { /// /// /// - [XmlRoot(RedmineKeys.USER)] - public class Watcher : IdentifiableName, IValue, ICloneable + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.ROLE)] + public sealed class CustomFieldRole : IdentifiableName { - #region IValue implementation /// - /// + /// Initializes a new instance of the class. /// - public string Value - { - get - { - return Id.ToString(); - } - } + /// Serialization + public CustomFieldRole() { } - #endregion - - /// - /// - /// - /// - public override string ToString() + internal CustomFieldRole(int id, string name) + : base(id, name) { - return string.Format("[Watcher: {0}]", base.ToString()); } /// /// /// /// - public object Clone() - { - var watcher = new Watcher { Id = Id, Name = Name }; - return watcher; - } + private string DebuggerDisplay => $"[CustomFieldRole: Id={Id.ToInvariantString()}, Name={Name}]"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/CustomFieldValue.cs b/src/redmine-net-api/Types/CustomFieldValue.cs new file mode 100644 index 00000000..eb2b20af --- /dev/null +++ b/src/redmine-net-api/Types/CustomFieldValue.cs @@ -0,0 +1,219 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Diagnostics; +using System.Xml; +using System.Xml.Schema; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.VALUE)] + public class CustomFieldValue : + IXmlSerializable + ,IJsonSerializable + ,IEquatable + ,ICloneable + { + /// + /// + /// + public CustomFieldValue() { } + + /// + /// + /// + /// + public CustomFieldValue(string value) + { + Info = value; + } + + #region Properties + + /// + /// + /// + public string Info { get; set; } + + #endregion + + #region Implementation of IXmlSerializable + + /// + /// + /// + /// + public XmlSchema GetSchema() + { + return null; + } + + /// + /// + /// + /// + public void ReadXml(XmlReader reader) + { + while (!reader.EOF) + { + if (reader.IsEmptyElement) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.VALUE: + Info = reader.ReadElementContentAsString(); + break; + + default: + reader.Read(); + break; + } + } + } + + /// + /// + /// + /// + public void WriteXml(XmlWriter writer) + { + } + + #endregion + + #region Implementation of IJsonSerialization + + /// + /// + /// + /// + public void ReadJson(JsonReader reader) + { + if (reader.TokenType == JsonToken.PropertyName) + { + return; + } + + if (reader.TokenType == JsonToken.String) + { + Info = reader.Value as string; + } + } + + /// + /// + /// + /// + public void WriteJson(JsonWriter writer) + { + } + + #endregion + + #region Implementation of IEquatable + + /// + /// + /// + /// + /// + public bool Equals(CustomFieldValue other) + { + if (other == null) return false; + return string.Equals(Info, other.Info, StringComparison.Ordinal); + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as CustomFieldValue); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Info, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(CustomFieldValue left, CustomFieldValue right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(CustomFieldValue left, CustomFieldValue right) + { + return !Equals(left, right); + } + + #endregion + + #region Implementation of IClonable + + /// + /// + /// + /// + public CustomFieldValue Clone(bool resetId) + { + return new CustomFieldValue { Info = Info }; + } + + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[CustomFieldValue: {Info}]"; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/Detail.cs b/src/redmine-net-api/Types/Detail.cs new file mode 100644 index 00000000..45a1217c --- /dev/null +++ b/src/redmine-net-api/Types/Detail.cs @@ -0,0 +1,254 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Diagnostics; +using System.Xml; +using System.Xml.Schema; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.DETAIL)] + public sealed class Detail : + IXmlSerializable + ,IJsonSerializable + ,IEquatable + ,ICloneable + { + /// + /// + /// + public Detail() { } + + internal Detail(string name = null, string property = null, string oldValue = null, string newValue = null) + { + Name = name; + Property = property; + OldValue = oldValue; + NewValue = newValue; + } + + #region Properties + /// + /// Gets the property. + /// + /// + /// The property. + /// + public string Property { get; internal set; } + + /// + /// Gets the name. + /// + /// + /// The name. + /// + public string Name { get; internal set; } + + /// + /// Gets the old value. + /// + /// + /// The old value. + /// + public string OldValue { get; internal set; } + + /// + /// Gets the new value. + /// + /// + /// The new value. + /// + public string NewValue { get; internal set; } + #endregion + + #region Implementation of IXmlSerialization + /// + /// + /// + /// + public XmlSchema GetSchema() { return null; } + + /// + /// + /// + /// + public void ReadXml(XmlReader reader) + { + Name = reader.GetAttribute(RedmineKeys.NAME); + Property = reader.GetAttribute(RedmineKeys.PROPERTY); + + reader.Read(); + + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.NEW_VALUE: NewValue = reader.ReadElementContentAsString(); break; + + case RedmineKeys.OLD_VALUE: OldValue = reader.ReadElementContentAsString(); break; + + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public void WriteXml(XmlWriter writer) { } + #endregion + + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public void WriteJson(JsonWriter writer) { } + + /// + /// + /// + /// + public void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + + case RedmineKeys.PROPERTY: Property = reader.ReadAsString(); break; + + case RedmineKeys.NEW_VALUE: NewValue = reader.ReadAsString(); break; + + case RedmineKeys.OLD_VALUE: OldValue = reader.ReadAsString(); break; + + default: reader.Read(); break; + } + } + } + #endregion + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public bool Equals(Detail other) + { + if (other == null) return false; + return string.Equals(Property, other.Property, StringComparison.Ordinal) + && string.Equals(Name, other.Name, StringComparison.Ordinal) + && string.Equals(OldValue, other.OldValue, StringComparison.Ordinal) + && string.Equals(NewValue, other.NewValue, StringComparison.Ordinal); + } + + /// + /// + /// + /// + /// + public Detail Clone(bool resetId) + { + return new Detail(Name, Property, OldValue, NewValue); + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as Detail); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Property, hashCode); + hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + hashCode = HashCodeHelper.GetHashCode(OldValue, hashCode); + hashCode = HashCodeHelper.GetHashCode(NewValue, hashCode); + + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Detail left, Detail right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Detail left, Detail right) + { + return !Equals(left, right); + } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[Detail: Property={Property}, Name={Name}, OldValue={OldValue}, NewValue={NewValue}]"; + + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/DocumentCategory.cs b/src/redmine-net-api/Types/DocumentCategory.cs new file mode 100644 index 00000000..d43637ac --- /dev/null +++ b/src/redmine-net-api/Types/DocumentCategory.cs @@ -0,0 +1,196 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// Availability 2.2 + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.DOCUMENT_CATEGORY)] + public sealed class DocumentCategory : IdentifiableName, IEquatable + { + /// + /// + /// + public DocumentCategory() { } + + internal DocumentCategory(int id, string name) + : base(id, name) + { + } + + #region Properties + /// + /// + /// + public bool IsDefault { get; internal set; } + + /// + /// + /// + public bool IsActive { get; internal set; } + #endregion + + #region Implementation of IXmlSerializable + + /// + /// Generates an object from its XML representation. + /// + /// The stream from which the object is deserialized. + public override void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.IS_DEFAULT: IsDefault = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; + case RedmineKeys.ACTIVE: IsActive = reader.ReadElementContentAsBoolean(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteXml(XmlWriter writer) { } + + #endregion + + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.IS_DEFAULT: IsDefault = reader.ReadAsBool(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.ACTIVE: IsActive = reader.ReadAsBool(); break; + default: reader.Read(); break; + } + } + } + #endregion + + #region Implementation of IEquatable + + /// + /// + /// + /// + /// + public bool Equals(DocumentCategory other) + { + if (other == null) return false; + + return base.Equals(other) + && IsDefault == other.IsDefault + && IsActive == other.IsActive; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as DocumentCategory); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(IsDefault, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsActive, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(DocumentCategory left, DocumentCategory right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(DocumentCategory left, DocumentCategory right) + { + return !Equals(left, right); + } + + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[DocumentCategory: Id={Id.ToInvariantString()}, Name={Name}, IsDefault={IsDefault.ToInvariantString()}, IsActive={IsActive.ToInvariantString()}]"; + + } +} \ No newline at end of file diff --git a/redmine-net20-api/Types/Error.cs b/src/redmine-net-api/Types/Error.cs old mode 100755 new mode 100644 similarity index 53% rename from redmine-net20-api/Types/Error.cs rename to src/redmine-net-api/Types/Error.cs index 463a8093..2c6db1c8 --- a/redmine-net20-api/Types/Error.cs +++ b/src/redmine-net-api/Types/Error.cs @@ -1,5 +1,5 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. +/* + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,46 +15,44 @@ limitations under the License. */ using System; +using System.Diagnostics; using System.Xml; using System.Xml.Schema; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json; namespace Redmine.Net.Api.Types { /// /// /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.ERROR)] - public class Error : IXmlSerializable, IEquatable + public sealed class Error : IXmlSerializable, IJsonSerializable, IEquatable { /// /// /// - [XmlText] - public string Info { get; set; } + public Error() { } /// /// /// - /// - /// - public bool Equals(Error other) + internal Error(string info) { - if (other == null) return false; - - return Info.Equals(other.Info); + Info = info; } + #region Properties /// /// /// - /// - public override string ToString() - { - return string.Format("[Error: Info={0}]", Info); - } + public string Info { get; private set; } + #endregion + #region Implementation of IXmlSerialization /// /// /// @@ -69,12 +67,6 @@ public void ReadXml(XmlReader reader) { while (!reader.EOF) { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - switch (reader.Name) { case RedmineKeys.ERROR: Info = reader.ReadElementContentAsString(); break; @@ -89,6 +81,46 @@ public void ReadXml(XmlReader reader) /// /// public void WriteXml(XmlWriter writer) { } + #endregion + + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public void ReadJson(JsonReader reader) + { + if (reader.TokenType == JsonToken.PropertyName) + { + reader.Read(); + } + + if (reader.TokenType == JsonToken.String) + { + Info = (string)reader.Value; + } + } + + /// + /// + /// + /// + public void WriteJson(JsonWriter writer) { } + #endregion + + #region Implementation of IEquatable + + /// + /// + /// + /// + /// + public bool Equals(Error other) + { + if (other == null) return false; + + return string.Equals(Info, other.Info, StringComparison.Ordinal); + } /// /// @@ -109,12 +141,39 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Info, hashCode); - return hashCode; - } + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Info, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Error left, Error right) + { + return Equals(left, right); } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Error left, Error right) + { + return !Equals(left, right); + } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[Error: {Info}]"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/File.cs b/src/redmine-net-api/Types/File.cs new file mode 100644 index 00000000..5b9efe39 --- /dev/null +++ b/src/redmine-net-api/Types/File.cs @@ -0,0 +1,291 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + + + +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using System; +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.FILE)] + public sealed class File : Identifiable + { + #region Properties + /// + /// + /// + public string Filename { get; set; } + + /// + /// + /// + public int FileSize { get; internal set; } + + /// + /// + /// + public string ContentType { get; internal set; } + + /// + /// + /// + public string Description { get; set; } + + /// + /// + /// + public string ContentUrl { get; internal set; } + + /// + /// + /// + public IdentifiableName Author { get; internal set; } + + /// + /// + /// + public DateTime? CreatedOn { get; internal set; } + + /// + /// + /// + public IdentifiableName Version { get; set; } + + /// + /// + /// + public string Digest { get; internal set; } + + /// + /// + /// + public int Downloads { get; internal set; } + + /// + /// + /// + public string Token { get; set; } + #endregion + + #region Implementation of IXmlSerializable + + /// + /// + /// + /// + public override void ReadXml(XmlReader reader) + { + reader.Read(); + + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; + case RedmineKeys.CONTENT_TYPE: ContentType = reader.ReadElementContentAsString(); break; + case RedmineKeys.CONTENT_URL: ContentUrl = reader.ReadElementContentAsString(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; + case RedmineKeys.DIGEST: Digest = reader.ReadElementContentAsString(); break; + case RedmineKeys.DOWNLOADS: Downloads = reader.ReadElementContentAsInt(); break; + case RedmineKeys.FILE_NAME: Filename = reader.ReadElementContentAsString(); break; + case RedmineKeys.FILE_SIZE: FileSize = reader.ReadElementContentAsInt(); break; + case RedmineKeys.TOKEN: Token = reader.ReadElementContentAsString(); break; + case RedmineKeys.VERSION: Version = new IdentifiableName(reader); break; + case RedmineKeys.VERSION_ID: Version = IdentifiableName.Create(reader.ReadElementContentAsInt()); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteXml(XmlWriter writer) + { + writer.WriteElementString(RedmineKeys.TOKEN, Token); + writer.WriteIdIfNotNull(RedmineKeys.VERSION_ID, Version); + writer.WriteElementString(RedmineKeys.FILE_NAME, Filename); + writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); + } + #endregion + + #region Implementation of IJsonSerializable + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt32().GetValueOrDefault(); break; + case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; + case RedmineKeys.CONTENT_TYPE: ContentType = reader.ReadAsString(); break; + case RedmineKeys.CONTENT_URL: ContentUrl = reader.ReadAsString(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadAsString(); break; + case RedmineKeys.DIGEST: Digest = reader.ReadAsString(); break; + case RedmineKeys.DOWNLOADS: Downloads = reader.ReadAsInt32().GetValueOrDefault(); break; + case RedmineKeys.FILE_NAME: Filename = reader.ReadAsString(); break; + case RedmineKeys.FILE_SIZE: FileSize = reader.ReadAsInt32().GetValueOrDefault(); break; + case RedmineKeys.TOKEN: Token = reader.ReadAsString(); break; + case RedmineKeys.VERSION: Version = new IdentifiableName(reader); break; + case RedmineKeys.VERSION_ID: Version = IdentifiableName.Create(reader.ReadAsInt32().GetValueOrDefault()); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.FILE)) + { + using (new JsonObject(writer)) + { + writer.WriteProperty(RedmineKeys.TOKEN, Token); + writer.WriteIdIfNotNull(RedmineKeys.VERSION_ID, Version); + writer.WriteProperty(RedmineKeys.FILE_NAME, Filename); + writer.WriteProperty(RedmineKeys.DESCRIPTION, Description); + } + } + } + #endregion + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public override bool Equals(File other) + { + if (other == null) return false; + return base.Equals(other) + && string.Equals(Filename, other.Filename, StringComparison.Ordinal) + && string.Equals(ContentType, other.ContentType, StringComparison.Ordinal) + && string.Equals(Description, other.Description, StringComparison.Ordinal) + && string.Equals(ContentUrl, other.ContentUrl, StringComparison.Ordinal) + && string.Equals(Digest, other.Digest, StringComparison.Ordinal) + && Author == other.Author + && FileSize == other.FileSize + && CreatedOn == other.CreatedOn + && Version == other.Version + && Downloads == other.Downloads; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as File); + } + + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Filename, hashCode); + hashCode = HashCodeHelper.GetHashCode(FileSize, hashCode); + hashCode = HashCodeHelper.GetHashCode(ContentType, hashCode); + hashCode = HashCodeHelper.GetHashCode(Description, hashCode); + hashCode = HashCodeHelper.GetHashCode(ContentUrl, hashCode); + hashCode = HashCodeHelper.GetHashCode(Author, hashCode); + hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(Version, hashCode); + hashCode = HashCodeHelper.GetHashCode(Digest, hashCode); + hashCode = HashCodeHelper.GetHashCode(Downloads, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(File left, File right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(File left, File right) + { + return !Equals(left, right); + } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[File: {Id.ToInvariantString()}, Name={Filename}]"; + + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs new file mode 100644 index 00000000..09152356 --- /dev/null +++ b/src/redmine-net-api/Types/Group.cs @@ -0,0 +1,243 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Common; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// Availability 2.1 + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.GROUP)] + public sealed class Group : IdentifiableName, IEquatable + { + /// + /// + /// + public Group() { } + + /// + /// + /// + /// + public Group(string name) + { + Name = name; + } + + #region Properties + /// + /// Represents the group's users. + /// + public List Users { get; set; } + + /// + /// Gets or sets the custom fields. + /// + /// The custom fields. + public List CustomFields { get; internal set; } + + /// + /// Gets or sets the custom fields. + /// + /// The custom fields. + public List Memberships { get; internal set; } + #endregion + + #region Implementation of IXmlSerializable + + /// + /// Generates an object from its XML representation. + /// + /// The stream from which the object is deserialized. + public override void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.MEMBERSHIPS: Memberships = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; + case RedmineKeys.USERS: Users = reader.ReadElementContentAsCollection(); break; + default: reader.Read(); break; + } + } + } + + /// + /// Converts an object into its XML representation. + /// + /// The stream to which the object is serialized. + public override void WriteXml(XmlWriter writer) + { + writer.WriteElementString(RedmineKeys.NAME, Name); + writer.WriteArrayIds(RedmineKeys.USER_IDS, Users, typeof(int), GetGroupUserId); + } + + #endregion + + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadAsCollection(); break; + case RedmineKeys.MEMBERSHIPS: Memberships = reader.ReadAsCollection(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.USERS: Users = reader.ReadAsCollection(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.GROUP)) + { + writer.WriteProperty(RedmineKeys.NAME, Name); + writer.WriteRepeatableElement(RedmineKeys.USER_IDS, (IEnumerable)Users); + } + } + #endregion + + #region Implementation of IEquatable + + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// + /// true if the current object is equal to the parameter; otherwise, false. + /// + /// An object to compare with this object. + public bool Equals(Group other) + { + if (other == null) return false; + return base.Equals(other) + && Users != null ? Users.Equals(other.Users) : other.Users == null + && CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null + && Memberships != null ? Memberships.Equals(other.Memberships) : other.Memberships == null; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as Group); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Users, hashCode); + hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); + hashCode = HashCodeHelper.GetHashCode(Memberships, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Group left, Group right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Group left, Group right) + { + return !Equals(left, right); + } + + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[Group: Id={Id.ToInvariantString()}, Name={Name}]"; + + + /// + /// + /// + /// + /// + public static int GetGroupUserId(object gu) + { + return ((GroupUser)gu).Id; + } + } +} \ No newline at end of file diff --git a/redmine-net20-api/Types/CustomFieldRole.cs b/src/redmine-net-api/Types/GroupUser.cs old mode 100755 new mode 100644 similarity index 58% rename from redmine-net20-api/Types/CustomFieldRole.cs rename to src/redmine-net-api/Types/GroupUser.cs index e0d0d58a..0151058b --- a/redmine-net20-api/Types/CustomFieldRole.cs +++ b/src/redmine-net-api/Types/GroupUser.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2017 Adrian Popescu. + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,23 +14,32 @@ You may obtain a copy of the License at limitations under the License. */ +using System.Diagnostics; using System.Xml.Serialization; +using Redmine.Net.Api.Common; +using Redmine.Net.Api.Extensions; namespace Redmine.Net.Api.Types { /// /// /// - [XmlRoot(RedmineKeys.ROLE)] - public class CustomFieldRole : IdentifiableName + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.USER)] + public sealed class GroupUser : IdentifiableName, IValue { + #region Implementation of IValue + /// + /// + /// + public string Value => Id.ToInvariantString(); + #endregion + /// /// /// /// - public override string ToString () - { - return string.Format ("[CustomFieldRole: {0}]", base.ToString()); - } + private string DebuggerDisplay => $"[GroupUser: Id={Id.ToInvariantString()}, Name={Name}]"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Identifiable.cs b/src/redmine-net-api/Types/Identifiable.cs new file mode 100644 index 00000000..2aa8a686 --- /dev/null +++ b/src/redmine-net-api/Types/Identifiable.cs @@ -0,0 +1,167 @@ +ο»Ώ/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Diagnostics; +using System.Xml; +using System.Xml.Schema; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + public abstract class Identifiable : IXmlSerializable, IJsonSerializable, IEquatable + , ICloneable> + where T : Identifiable + { + #region Properties + /// + /// Gets the id. + /// + /// The id. + public int Id { get; protected internal set; } + #endregion + + #region Implementation of IXmlSerialization + /// + /// + /// + /// + public XmlSchema GetSchema() { return null; } + + /// + /// + /// + /// + public virtual void ReadXml(XmlReader reader) { } + + /// + /// + /// + /// + public virtual void WriteXml(XmlWriter writer) { } + #endregion + + #region Implementation of IJsonSerializable + /// + /// + /// + /// + public virtual void ReadJson(JsonReader reader) { } + + /// + /// + /// + /// + public virtual void WriteJson(JsonWriter writer) { } + #endregion + + #region Implementation of IEquatable> + /// + /// + /// + /// + /// + public bool Equals(Identifiable other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return Id == other.Id; + } + + /// + /// + /// + /// + /// + public virtual bool Equals(T other) + { + if (other == null) return false; + return Id == other.Id; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as Identifiable); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Id, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Identifiable left, Identifiable right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Identifiable left, Identifiable right) + { + return !Equals(left, right); + } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"Id={Id.ToInvariantString()}"; + + /// + /// + /// + /// + public virtual Identifiable Clone(bool resetId) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/IdentifiableName.cs b/src/redmine-net-api/Types/IdentifiableName.cs new file mode 100644 index 00000000..04d87997 --- /dev/null +++ b/src/redmine-net-api/Types/IdentifiableName.cs @@ -0,0 +1,269 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Diagnostics; +using System.Xml; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + public class IdentifiableName : Identifiable + , ICloneable + { + /// + /// + /// + /// + /// + public static T Create(int id) where T: IdentifiableName, new() + { + var t = new T + { + Id = id + }; + return t; + } + + internal static T Create(int id, string name) where T: IdentifiableName, new() + { + var t = new T + { + Id = id, Name = name + }; + return t; + } + + /// + /// Initializes a new instance of the class. + /// + public IdentifiableName() { } + + /// + /// Initializes the class by using the given Id and Name. + /// + /// The Id. + /// The Name. + internal IdentifiableName(int id, string name) + { + Id = id; + Name = name; + } + + /// + /// Initializes a new instance of the class. + /// + /// The reader. + public IdentifiableName(XmlReader reader) + { + Initialize(reader); + } + + /// + /// + /// + /// + public IdentifiableName(JsonReader reader) + { + Initialize(reader); + } + + private void Initialize(XmlReader reader) + { + ReadXml(reader); + } + + private void Initialize(JsonReader reader) + { + ReadJson(reader); + } + + #region Properties + /// + /// Gets or sets the name. + /// + public virtual string Name { get; set; } + #endregion + + #region Implementation of IXmlSerializable + + /// + /// + /// + /// + public override void ReadXml(XmlReader reader) + { + Id = reader.ReadAttributeAsInt(RedmineKeys.ID); + Name = reader.GetAttribute(RedmineKeys.NAME); + reader.Read(); + } + + /// + /// + /// + /// + public override void WriteXml(XmlWriter writer) + { + writer.WriteAttributeString(RedmineKeys.ID, Id.ToInvariantString()); + writer.WriteAttributeString(RedmineKeys.NAME, Name); + } + + #endregion + + #region Implementation of IJsonSerializable + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType == JsonToken.PropertyName) + { + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + } + + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + writer.WriteIdIfNotNull(RedmineKeys.ID, this); + if (!Name.IsNullOrWhiteSpace()) + { + writer.WriteProperty(RedmineKeys.NAME, Name); + } + } + #endregion + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public override bool Equals(IdentifiableName other) + { + if (other == null) return false; + return Id == other.Id && string.Equals(Name, other.Name, StringComparison.Ordinal); + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as IdentifiableName); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(IdentifiableName left, IdentifiableName right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(IdentifiableName left, IdentifiableName right) + { + return !Equals(left, right); + } + #endregion + + /// + /// + /// + /// + /// + public static implicit operator string(IdentifiableName identifiableName) => FromIdentifiableName(identifiableName); + + /// + /// + /// + /// + /// + public static string FromIdentifiableName(IdentifiableName identifiableName) + { + return identifiableName?.Id.ToInvariantString(); + } + + /// + /// + /// + /// + private string DebuggerDisplay => $"[IdentifiableName: Id={Id.ToInvariantString()}, Name={Name}]"; + + /// + /// + /// + /// + public new IdentifiableName Clone(bool resetId) + { + return new IdentifiableName + { + Id = Id, + Name = Name + }; + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/Include.cs b/src/redmine-net-api/Types/Include.cs new file mode 100644 index 00000000..1c8b2d9c --- /dev/null +++ b/src/redmine-net-api/Types/Include.cs @@ -0,0 +1,147 @@ +namespace Redmine.Net.Api.Types; + +/// +/// +/// + [System.Diagnostics.CodeAnalysis.SuppressMessage( +"Design", +"CA1034:Nested types should not be visible", +Justification = "Deliberately exposed")] + +public static class Include +{ + /// + /// + /// + public static class Group + { + /// + /// + /// + public const string Users = RedmineKeys.USERS; + + /// + /// Adds extra information about user's memberships and roles on the projects + /// + public const string Memberships = RedmineKeys.MEMBERSHIPS; + } + + /// + /// Associated data that can be retrieved + /// + public static class Issue + { + /// + /// Specifies whether to include child issues. + /// This parameter is applicable when retrieving details for a specific issue. + /// Corresponds to the Redmine API include parameter: children. + /// + public const string Children = RedmineKeys.CHILDREN; + + /// + /// Specifies whether to include attachments. + /// This parameter is applicable when retrieving a list of issues or details for a specific issue. + /// Corresponds to the Redmine API include parameter: attachments. + /// + public const string Attachments = RedmineKeys.ATTACHMENTS; + + /// + /// Specifies whether to include issue relations. + /// This parameter is applicable when retrieving a list of issues or details for a specific issue. + /// Corresponds to the Redmine API include parameter: relations. + /// + public const string Relations = RedmineKeys.RELATIONS; + + /// + /// Specifies whether to include associated changesets. + /// This parameter is applicable when retrieving details for a specific issue. + /// Corresponds to the Redmine API include parameter: changesets. + /// + public const string Changesets = RedmineKeys.CHANGE_SETS; + + /// + /// Specifies whether to include journal entries (notes and history). + /// This parameter is applicable when retrieving details for a specific issue. + /// Corresponds to the Redmine API include parameter: journals. + /// + public const string Journals = RedmineKeys.JOURNALS; + + /// + /// Specifies whether to include watchers of the issue. + /// This parameter is applicable when retrieving details for a specific issue. + /// Corresponds to the Redmine API include parameter: watchers. + /// + public const string Watchers = RedmineKeys.WATCHERS; + + /// + /// Specifies whether to include allowed statuses of the issue. + /// This parameter is applicable when retrieving details for a specific issue. + /// Corresponds to the Redmine API include parameter: watchers. + /// Since 5.0.x, Returns the available allowed statuses (the same values as provided in the issue edit form) based on: + /// the issue's current tracker, the issue's current status, and the member's role (the defined workflow); + /// the existence of any open subtask(s); + /// the existence of any open blocking issue(s); + /// the existence of a closed parent issue. + /// + public const string AllowedStatuses = RedmineKeys.ALLOWED_STATUSES; + } + + /// + /// + /// + public static class Project + { + /// + /// + /// + public const string Trackers = RedmineKeys.TRACKERS; + + /// + /// since 2.6.0 + /// + public const string EnabledModules = RedmineKeys.ENABLED_MODULES; + + /// + /// + /// + public const string IssueCategories = RedmineKeys.ISSUE_CATEGORIES; + + /// + /// since 3.4.0 + /// + public const string TimeEntryActivities = RedmineKeys.TIME_ENTRY_ACTIVITIES; + + /// + /// since 4.2.0 + /// + public const string IssueCustomFields = RedmineKeys.ISSUE_CUSTOM_FIELDS; + } + + /// + /// + /// + public static class User + { + /// + /// Adds extra information about user's memberships and roles on the projects + /// + public const string Memberships = RedmineKeys.MEMBERSHIPS; + + /// + /// Adds extra information about user's groups + /// added in 2.1 + /// + public const string Groups = RedmineKeys.GROUPS; + } + + /// + /// + /// + public static class WikiPage + { + /// + /// + /// + public const string Attachments = RedmineKeys.ATTACHMENTS; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs new file mode 100644 index 00000000..4cfbc308 --- /dev/null +++ b/src/redmine-net-api/Types/Issue.cs @@ -0,0 +1,658 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Common; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + /// + /// Available as of 1.1 : + /// include: fetch associated data (optional). + /// Possible values: children, attachments, relations, changesets and journals. To fetch multiple associations use comma (e.g ?include=relations,journals). + /// See Issue journals for more information. + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.ISSUE)] + public sealed class Issue : + Identifiable + ,ICloneable + { + #region Properties + /// + /// Gets or sets the project. + /// + /// The project. + public IdentifiableName Project { get; set; } + + /// + /// Gets or sets the tracker. + /// + /// The tracker. + public IdentifiableName Tracker { get; set; } + + /// + /// Gets or sets the status. Possible values: open, closed, * to get open and closed issues, status id + /// + /// The status. + public IssueStatus Status { get; set; } + + /// + /// Gets or sets the priority. + /// + /// The priority. + public IdentifiableName Priority { get; set; } + + /// + /// Gets or sets the author. + /// + /// The author. + public IdentifiableName Author { get; set; } + + /// + /// Gets or sets the category. + /// + /// The category. + public IdentifiableName Category { get; set; } + + /// + /// Gets or sets the subject. + /// + /// The subject. + public string Subject { get; set; } + + /// + /// Gets or sets the description. + /// + /// The description. + public string Description { get; set; } + + /// + /// Gets or sets the start date. + /// + /// The start date. + public DateTime? StartDate { get; set; } + + /// + /// Gets or sets the due date. + /// + /// The due date. + public DateTime? DueDate { get; set; } + + /// + /// Gets or sets the done ratio. + /// + /// The done ratio. + public float? DoneRatio { get; set; } + + /// + /// Gets or sets a value indicating whether [private notes]. + /// + /// + /// true if [private notes]; otherwise, false. + /// + public bool PrivateNotes { get; set; } + + /// + /// Gets or sets the estimated hours. + /// + /// The estimated hours. + public float? EstimatedHours { get; set; } + + /// + /// Gets or sets the hours spent on the issue. + /// + /// The hours spent on the issue. + public float? SpentHours { get; set; } + + /// + /// Gets or sets the custom fields. + /// + /// The custom fields. + public List CustomFields { get; set; } + + /// + /// Gets or sets the created on. + /// + /// The created on. + public DateTime? CreatedOn { get; set; } + + /// + /// Gets or sets the updated on. + /// + /// The updated on. + public DateTime? UpdatedOn { get; internal set; } + + /// + /// Gets or sets the closed on. + /// + /// The closed on. + public DateTime? ClosedOn { get; internal set; } + + /// + /// Gets or sets the notes. + /// + public string Notes { get; set; } + + /// + /// Gets or sets the ID of the user to assign the issue to (currently no mechanism to assign by name). + /// + /// + /// The assigned to. + /// + public IdentifiableName AssignedTo { get; set; } + + /// + /// Gets or sets the parent issue id. Only when a new issue is created this property shall be used. + /// + /// + /// The parent issue id. + /// + public IdentifiableName ParentIssue { get; set; } + + /// + /// Gets or sets the fixed version. + /// + /// + /// The fixed version. + /// + public IdentifiableName FixedVersion { get; set; } + + /// + /// indicate whether the issue is private or not + /// + /// + /// true if this issue is private; otherwise, false. + /// + public bool IsPrivate { get; set; } + + /// + /// Returns the sum of spent hours of the task and all the sub tasks. + /// + /// Availability starting with redmine version 3.3 + public float? TotalSpentHours { get; set; } + + /// + /// Returns the sum of estimated hours of task and all the sub tasks. + /// + /// Availability starting with redmine version 3.3 + public float? TotalEstimatedHours { get; set; } + + /// + /// Gets or sets the journals. + /// + /// + /// The journals. + /// + public List Journals { get; set; } + + /// + /// Gets or sets the change sets. + /// + /// + /// The change sets. + /// + public List ChangeSets { get; set; } + + /// + /// Gets or sets the attachments. + /// + /// + /// The attachments. + /// + public List Attachments { get; set; } + + /// + /// Gets or sets the issue relations. + /// + /// + /// The issue relations. + /// + public List Relations { get; set; } + + /// + /// Gets or sets the issue children. + /// + /// + /// The issue children. + /// NOTE: Only Id, tracker and subject are filled. + /// + public List Children { get; set; } + + /// + /// Gets or sets the attachments. + /// + /// + /// The attachment. + /// + public List Uploads { get; set; } + + /// + /// + /// + public List Watchers { get; set; } + + /// + /// + /// + public List AllowedStatuses { get; set; } + #endregion + + #region Implementation of IXmlSerialization + /// + /// + /// + /// + public override void ReadXml(XmlReader reader) + { + reader.Read(); + + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.ALLOWED_STATUSES: AllowedStatuses = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.ASSIGNED_TO: AssignedTo = new IdentifiableName(reader); break; + case RedmineKeys.ATTACHMENTS: Attachments = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; + case RedmineKeys.CATEGORY: Category = new IdentifiableName(reader); break; + case RedmineKeys.CHANGE_SETS: ChangeSets = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.CHILDREN: Children = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.CLOSED_ON: ClosedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; + case RedmineKeys.DONE_RATIO: DoneRatio = reader.ReadElementContentAsNullableFloat(); break; + case RedmineKeys.DUE_DATE: DueDate = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.ESTIMATED_HOURS: EstimatedHours = reader.ReadElementContentAsNullableFloat(); break; + case RedmineKeys.FIXED_VERSION: FixedVersion = new IdentifiableName(reader); break; + case RedmineKeys.IS_PRIVATE: IsPrivate = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.JOURNALS: Journals = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.NOTES: Notes = reader.ReadElementContentAsString(); break; + case RedmineKeys.PARENT: ParentIssue = new IdentifiableName(reader); break; + case RedmineKeys.PRIORITY: Priority = new IdentifiableName(reader); break; + case RedmineKeys.PRIVATE_NOTES: PrivateNotes = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; + case RedmineKeys.RELATIONS: Relations = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.SPENT_HOURS: SpentHours = reader.ReadElementContentAsNullableFloat(); break; + case RedmineKeys.START_DATE: StartDate = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.STATUS: Status = new IssueStatus(reader); break; + case RedmineKeys.SUBJECT: Subject = reader.ReadElementContentAsString(); break; + case RedmineKeys.TOTAL_ESTIMATED_HOURS: TotalEstimatedHours = reader.ReadElementContentAsNullableFloat(); break; + case RedmineKeys.TOTAL_SPENT_HOURS: TotalSpentHours = reader.ReadElementContentAsNullableFloat(); break; + case RedmineKeys.TRACKER: Tracker = new IdentifiableName(reader); break; + case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.WATCHERS: Watchers = reader.ReadElementContentAsCollection(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteXml(XmlWriter writer) + { + writer.WriteElementString(RedmineKeys.SUBJECT, Subject); + writer.WriteElementString(RedmineKeys.NOTES, Notes); + + if (Id != 0) + { + writer.WriteBoolean(RedmineKeys.PRIVATE_NOTES, PrivateNotes); + } + + writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); + writer.WriteElementString(RedmineKeys.IS_PRIVATE, IsPrivate.ToInvariantString()); + + writer.WriteIdIfNotNull(RedmineKeys.PROJECT_ID, Project); + writer.WriteIdIfNotNull(RedmineKeys.PRIORITY_ID, Priority); + writer.WriteIdIfNotNull(RedmineKeys.STATUS_ID, Status); + writer.WriteIdIfNotNull(RedmineKeys.CATEGORY_ID, Category); + writer.WriteIdIfNotNull(RedmineKeys.TRACKER_ID, Tracker); + writer.WriteIdIfNotNull(RedmineKeys.ASSIGNED_TO_ID, AssignedTo); + writer.WriteIdIfNotNull(RedmineKeys.PARENT_ISSUE_ID, ParentIssue); + writer.WriteIdIfNotNull(RedmineKeys.FIXED_VERSION_ID, FixedVersion); + writer.WriteValueOrEmpty(RedmineKeys.ESTIMATED_HOURS, EstimatedHours); + writer.WriteIfNotDefaultOrNull(RedmineKeys.DONE_RATIO, DoneRatio); + + writer.WriteDateOrEmpty(RedmineKeys.START_DATE, StartDate); + writer.WriteDateOrEmpty(RedmineKeys.DUE_DATE, DueDate); + writer.WriteDateOrEmpty(RedmineKeys.UPDATED_ON, UpdatedOn); + + writer.WriteArray(RedmineKeys.UPLOADS, Uploads); + writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); + + writer.WriteListElements(RedmineKeys.WATCHER_USER_IDS, (IEnumerable)Watchers); + } + #endregion + + #region Implementation of IJsonSerializable + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt32().GetValueOrDefault(); break; + case RedmineKeys.ALLOWED_STATUSES: AllowedStatuses = reader.ReadAsCollection(); break; + case RedmineKeys.ASSIGNED_TO: AssignedTo = new IdentifiableName(reader); break; + case RedmineKeys.ATTACHMENTS: Attachments = reader.ReadAsCollection(); break; + case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; + case RedmineKeys.CATEGORY: Category = new IdentifiableName(reader); break; + case RedmineKeys.CHANGE_SETS: ChangeSets = reader.ReadAsCollection(); break; + case RedmineKeys.CHILDREN: Children = reader.ReadAsCollection(); break; + case RedmineKeys.CLOSED_ON: ClosedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadAsCollection(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadAsString(); break; + case RedmineKeys.DONE_RATIO: DoneRatio = (float?)reader.ReadAsDouble(); break; + case RedmineKeys.DUE_DATE: DueDate = reader.ReadAsDateTime(); break; + case RedmineKeys.ESTIMATED_HOURS: EstimatedHours = (float?)reader.ReadAsDouble(); break; + case RedmineKeys.FIXED_VERSION: FixedVersion = new IdentifiableName(reader); break; + case RedmineKeys.IS_PRIVATE: IsPrivate = reader.ReadAsBoolean().GetValueOrDefault(); break; + case RedmineKeys.JOURNALS: Journals = reader.ReadAsCollection(); break; + case RedmineKeys.NOTES: Notes = reader.ReadAsString(); break; + case RedmineKeys.PARENT: ParentIssue = new IdentifiableName(reader); break; + case RedmineKeys.PRIORITY: Priority = new IdentifiableName(reader); break; + case RedmineKeys.PRIVATE_NOTES: PrivateNotes = reader.ReadAsBoolean().GetValueOrDefault(); break; + case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; + case RedmineKeys.RELATIONS: Relations = reader.ReadAsCollection(); break; + case RedmineKeys.SPENT_HOURS: SpentHours = (float?)reader.ReadAsDouble(); break; + case RedmineKeys.START_DATE: StartDate = reader.ReadAsDateTime(); break; + case RedmineKeys.STATUS: Status = new IssueStatus(reader); break; + case RedmineKeys.SUBJECT: Subject = reader.ReadAsString(); break; + case RedmineKeys.TOTAL_ESTIMATED_HOURS: TotalEstimatedHours = (float?)reader.ReadAsDouble(); break; + case RedmineKeys.TOTAL_SPENT_HOURS: TotalSpentHours = (float?)reader.ReadAsDouble(); break; + case RedmineKeys.TRACKER: Tracker = new IdentifiableName(reader); break; + case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.WATCHERS: Watchers = reader.ReadAsCollection(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.ISSUE)) + { + writer.WriteProperty(RedmineKeys.SUBJECT, Subject); + writer.WriteProperty(RedmineKeys.DESCRIPTION, Description); + writer.WriteProperty(RedmineKeys.NOTES, Notes); + + if (Id != 0) + { + writer.WriteBoolean(RedmineKeys.PRIVATE_NOTES, PrivateNotes); + } + + writer.WriteBoolean(RedmineKeys.IS_PRIVATE, IsPrivate); + writer.WriteIdIfNotNull(RedmineKeys.PROJECT_ID, Project); + writer.WriteIdIfNotNull(RedmineKeys.PRIORITY_ID, Priority); + writer.WriteIdIfNotNull(RedmineKeys.STATUS_ID, Status); + writer.WriteIdIfNotNull(RedmineKeys.CATEGORY_ID, Category); + writer.WriteIdIfNotNull(RedmineKeys.TRACKER_ID, Tracker); + writer.WriteIdIfNotNull(RedmineKeys.ASSIGNED_TO_ID, AssignedTo); + writer.WriteIdIfNotNull(RedmineKeys.FIXED_VERSION_ID, FixedVersion); + writer.WriteValueOrEmpty(RedmineKeys.ESTIMATED_HOURS, EstimatedHours); + + writer.WriteIdOrEmpty(RedmineKeys.PARENT_ISSUE_ID, ParentIssue); + writer.WriteDateOrEmpty(RedmineKeys.START_DATE, StartDate); + writer.WriteDateOrEmpty(RedmineKeys.DUE_DATE, DueDate); + writer.WriteDateOrEmpty(RedmineKeys.UPDATED_ON, UpdatedOn); + + if (DoneRatio != null) + { + writer.WriteProperty(RedmineKeys.DONE_RATIO, DoneRatio.Value.ToInvariantString()); + } + + if (SpentHours != null) + { + writer.WriteProperty(RedmineKeys.SPENT_HOURS, SpentHours.Value.ToInvariantString()); + } + + writer.WriteArray(RedmineKeys.UPLOADS, Uploads); + writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); + + writer.WriteRepeatableElement(RedmineKeys.WATCHER_USER_IDS, (IEnumerable)Watchers); + } + } + #endregion + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public override bool Equals(Issue other) + { + if (other == null) return false; + return Id == other.Id + && Project == other.Project + && Tracker == other.Tracker + && Status == other.Status + && Priority == other.Priority + && Author == other.Author + && Category == other.Category + && string.Equals(Subject, other.Subject, StringComparison.Ordinal) + && string.Equals(Description, other.Description, StringComparison.Ordinal) + && StartDate == other.StartDate + && DueDate == other.DueDate + && DoneRatio == other.DoneRatio + && EstimatedHours == other.EstimatedHours + && SpentHours == other.SpentHours + && CreatedOn == other.CreatedOn + && UpdatedOn == other.UpdatedOn + && AssignedTo == other.AssignedTo + && FixedVersion == other.FixedVersion + && string.Equals(Notes, other.Notes, StringComparison.Ordinal) + && ClosedOn == other.ClosedOn + && PrivateNotes == other.PrivateNotes + && (Attachments?.Equals(other.Attachments) ?? other.Attachments == null) + && (CustomFields?.Equals(other.CustomFields) ?? other.CustomFields == null) + && (ChangeSets?.Equals(other.ChangeSets) ?? other.ChangeSets == null) + && (Children?.Equals(other.Children) ?? other.Children == null) + && (Journals?.Equals(other.Journals) ?? other.Journals == null) + && (Relations?.Equals(other.Relations) ?? other.Relations == null) + && (Watchers?.Equals(other.Watchers) ?? other.Watchers == null); + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as Issue); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + + hashCode = HashCodeHelper.GetHashCode(Project, hashCode); + + hashCode = HashCodeHelper.GetHashCode(Tracker, hashCode); + hashCode = HashCodeHelper.GetHashCode(Status, hashCode); + hashCode = HashCodeHelper.GetHashCode(Priority, hashCode); + hashCode = HashCodeHelper.GetHashCode(Author, hashCode); + hashCode = HashCodeHelper.GetHashCode(Category, hashCode); + + hashCode = HashCodeHelper.GetHashCode(Subject, hashCode); + hashCode = HashCodeHelper.GetHashCode(Description, hashCode); + hashCode = HashCodeHelper.GetHashCode(StartDate, hashCode); + hashCode = HashCodeHelper.GetHashCode(Project, hashCode); + hashCode = HashCodeHelper.GetHashCode(DueDate, hashCode); + hashCode = HashCodeHelper.GetHashCode(DoneRatio, hashCode); + hashCode = HashCodeHelper.GetHashCode(PrivateNotes, hashCode); + hashCode = HashCodeHelper.GetHashCode(EstimatedHours, hashCode); + hashCode = HashCodeHelper.GetHashCode(SpentHours, hashCode); + hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); + + hashCode = HashCodeHelper.GetHashCode(Notes, hashCode); + hashCode = HashCodeHelper.GetHashCode(AssignedTo, hashCode); + hashCode = HashCodeHelper.GetHashCode(ParentIssue, hashCode); + hashCode = HashCodeHelper.GetHashCode(FixedVersion, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsPrivate, hashCode); + hashCode = HashCodeHelper.GetHashCode(Journals, hashCode); + hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); + + hashCode = HashCodeHelper.GetHashCode(ChangeSets, hashCode); + hashCode = HashCodeHelper.GetHashCode(Attachments, hashCode); + hashCode = HashCodeHelper.GetHashCode(Relations, hashCode); + hashCode = HashCodeHelper.GetHashCode(Children, hashCode); + hashCode = HashCodeHelper.GetHashCode(Uploads, hashCode); + hashCode = HashCodeHelper.GetHashCode(Watchers, hashCode); + + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Issue left, Issue right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Issue left, Issue right) + { + return !Equals(left, right); + } + #endregion + + #region Implementation of IClonable + /// + /// + /// + /// + public new Issue Clone(bool resetId) + { + var issue = new Issue + { + Project = Project?.Clone(false), + Tracker = Tracker?.Clone(false), + Status = Status?.Clone(false), + Priority = Priority?.Clone(false), + Author = Author?.Clone(false), + Category = Category?.Clone(false), + Subject = Subject, + Description = Description, + StartDate = StartDate, + DueDate = DueDate, + DoneRatio = DoneRatio, + IsPrivate = IsPrivate, + EstimatedHours = EstimatedHours, + TotalEstimatedHours = TotalEstimatedHours, + SpentHours = SpentHours, + TotalSpentHours = TotalSpentHours, + AssignedTo = AssignedTo?.Clone(false), + FixedVersion = FixedVersion?.Clone(false), + Notes = Notes, + PrivateNotes = PrivateNotes, + CreatedOn = CreatedOn, + UpdatedOn = UpdatedOn, + ClosedOn = ClosedOn, + ParentIssue = ParentIssue?.Clone(false), + CustomFields = CustomFields?.Clone(false), + Journals = Journals?.Clone(false), + Attachments = Attachments?.Clone(false), + Relations = Relations?.Clone(false), + Children = Children?.Clone(false), + Watchers = Watchers?.Clone(false), + Uploads = Uploads?.Clone(false), + }; + + return issue; + } + + #endregion + + /// + /// + /// + /// + public IdentifiableName AsParent() + { + return IdentifiableName.Create(Id); + } + + /// + /// Provides a string representation of the object for use in debugging. + /// + /// + /// A string that represents the object, formatted for debugging purposes. + /// + private string DebuggerDisplay => $"[Issue:Id={Id.ToInvariantString()}, Status={Status?.Name}, Priority={Priority?.Name}, DoneRatio={DoneRatio?.ToString("F", CultureInfo.InvariantCulture)},IsPrivate={IsPrivate.ToInvariantString()}]"; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssueAllowedStatus.cs b/src/redmine-net-api/Types/IssueAllowedStatus.cs new file mode 100644 index 00000000..14ecdb72 --- /dev/null +++ b/src/redmine-net-api/Types/IssueAllowedStatus.cs @@ -0,0 +1,133 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.STATUS)] + public sealed class IssueAllowedStatus : IdentifiableName + { + /// + /// + /// + public bool? IsClosed { get; internal set; } + + /// + public override void ReadXml(XmlReader reader) + { + Id = reader.ReadAttributeAsInt(RedmineKeys.ID); + Name = reader.GetAttribute(RedmineKeys.NAME); + IsClosed = reader.ReadAttributeAsBoolean(RedmineKeys.IS_CLOSED); + reader.Read(); + } + + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType == JsonToken.PropertyName) + { + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.IS_CLOSED: IsClosed = reader.ReadAsBoolean(); break; + default: reader.Read(); break; + } + } + } + } + + /// + /// + /// + /// + /// + public bool Equals(IssueAllowedStatus other) + { + if (other == null) return false; + return Id == other.Id + && Name == other.Name + && IsClosed == other.IsClosed; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as IssueAllowedStatus); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(IsClosed, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(IssueAllowedStatus left, IssueAllowedStatus right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(IssueAllowedStatus left, IssueAllowedStatus right) + { + return !Equals(left, right); + } + + private string DebuggerDisplay => $"[IssueAllowedStatus: Id={Id.ToInvariantString()}, Name={Name}]"; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssueCategory.cs b/src/redmine-net-api/Types/IssueCategory.cs new file mode 100644 index 00000000..f31a64ed --- /dev/null +++ b/src/redmine-net-api/Types/IssueCategory.cs @@ -0,0 +1,214 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// Availability 1.3 + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.ISSUE_CATEGORY)] + public sealed class IssueCategory : Identifiable + { + #region Properties + /// + /// Gets or sets the project. + /// + /// + /// The project. + /// + public IdentifiableName Project { get; set; } + + /// + /// Gets or sets the asign to. + /// + /// + /// The asign to. + /// + public IdentifiableName AssignTo { get; set; } + + /// + /// Gets or sets the name. + /// + /// + /// The name. + /// + public string Name { get; set; } + #endregion + + #region Implementation of IXmlSerialization + /// + /// + /// + /// + public override void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.ASSIGNED_TO: AssignTo = new IdentifiableName(reader); break; + case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; + case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteXml(XmlWriter writer) + { + // writer.WriteIdIfNotNull(RedmineKeys.PROJECT_ID, Project); + writer.WriteElementString(RedmineKeys.NAME, Name); + writer.WriteIdIfNotNull(RedmineKeys.ASSIGNED_TO_ID, AssignTo); + } + #endregion + + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.ASSIGNED_TO: AssignTo = new IdentifiableName(reader); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.ISSUE_CATEGORY)) + { + writer.WriteIdIfNotNull(RedmineKeys.PROJECT_ID, Project); + writer.WriteProperty(RedmineKeys.NAME, Name); + writer.WriteIdIfNotNull(RedmineKeys.ASSIGNED_TO_ID, AssignTo); + } + } + #endregion + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public override bool Equals(IssueCategory other) + { + if (other == null) return false; + return Id == other.Id && Project == other.Project && AssignTo == other.AssignTo && Name == other.Name; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as IssueCategory); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Project, hashCode); + hashCode = HashCodeHelper.GetHashCode(AssignTo, hashCode); + hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(IssueCategory left, IssueCategory right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(IssueCategory left, IssueCategory right) + { + return !Equals(left, right); + } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[IssueCategory: Id={Id.ToInvariantString()}, Name={Name}]"; + + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssueChild.cs b/src/redmine-net-api/Types/IssueChild.cs new file mode 100644 index 00000000..fa903468 --- /dev/null +++ b/src/redmine-net-api/Types/IssueChild.cs @@ -0,0 +1,194 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Diagnostics; +using System.Globalization; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.ISSUE)] + public sealed class IssueChild : Identifiable + ,ICloneable + { + #region Properties + /// + /// Gets or sets the tracker. + /// + /// The tracker. + public IdentifiableName Tracker { get; internal set; } + + /// + /// Gets or sets the subject. + /// + /// The subject. + public string Subject { get; internal set; } + #endregion + + #region Implementation of IXmlSerialization + /// + /// + /// + /// + public override void ReadXml(XmlReader reader) + { + Id = Convert.ToInt32(reader.GetAttribute(RedmineKeys.ID), CultureInfo.InvariantCulture); + reader.Read(); + + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.SUBJECT: Subject = reader.ReadElementContentAsString(); break; + case RedmineKeys.TRACKER: Tracker = new IdentifiableName(reader); break; + default: reader.Read(); break; + } + } + } + #endregion + + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.SUBJECT: Subject = reader.ReadAsString(); break; + case RedmineKeys.TRACKER: Tracker = new IdentifiableName(reader); break; + default: reader.Read(); break; + } + } + } + #endregion + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public override bool Equals(IssueChild other) + { + if (other == null) return false; + return base.Equals(other) + && Tracker == other.Tracker + && string.Equals(Subject, other.Subject, StringComparison.Ordinal); + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as IssueChild); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Tracker, hashCode); + hashCode = HashCodeHelper.GetHashCode(Subject, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(IssueChild left, IssueChild right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(IssueChild left, IssueChild right) + { + return !Equals(left, right); + } + #endregion + + #region Implementation of IClonable + /// + /// + /// + /// + public new IssueChild Clone(bool resetId) + { + return new IssueChild + { + Id = Id, + Tracker = Tracker?.Clone(false), + Subject = Subject + }; + } + + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[IssueChild: Id={Id.ToInvariantString()}]"; + } +} diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs new file mode 100644 index 00000000..e9923f79 --- /dev/null +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -0,0 +1,378 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Common; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.CUSTOM_FIELD)] + public sealed class IssueCustomField : + IdentifiableName + ,IEquatable + ,ICloneable, IValue + { + #region Properties + /// + /// Gets or sets the value. + /// + /// The value. + public List Values { get; set; } + + /// + /// + /// + public bool Multiple { get; set; } + #endregion + + #region Implementation of IXmlSerializable + + /// + /// + /// + /// + public override void ReadXml(XmlReader reader) + { + Id = Convert.ToInt32(reader.GetAttribute(RedmineKeys.ID), CultureInfo.InvariantCulture); + Name = reader.GetAttribute(RedmineKeys.NAME); + Multiple = reader.ReadAttributeAsBoolean(RedmineKeys.MULTIPLE); + + reader.Read(); + + if (reader.NodeType == XmlNodeType.Whitespace) + { + reader.Read(); + } + + if (reader.NodeType == XmlNodeType.Text) + { + Values = new List + { + new CustomFieldValue(reader.Value) + }; + + reader.Read(); + return; + } + + var attributeExists = !reader.GetAttribute("type").IsNullOrWhiteSpace(); + + if (!attributeExists) + { + if (reader.IsEmptyElement) + { + reader.Read(); + return; + } + + Values = new List + { + new CustomFieldValue(reader.ReadElementContentAsString()) + }; + } + else + { + Values = reader.ReadElementContentAsCollection(); + } + } + + /// + /// + /// + /// + public override void WriteXml(XmlWriter writer) + { + if (Values == null) + { + return; + } + + var itemsCount = Values.Count; + + writer.WriteAttributeString(RedmineKeys.ID, Id.ToInvariantString()); + + Multiple = itemsCount > 1; + + if (Multiple) + { + writer.WriteArrayStringElement(RedmineKeys.VALUE, Values, GetValue); + } + else + { + writer.WriteElementString(RedmineKeys.VALUE, itemsCount > 0 ? Values[0].Info : null); + } + + writer.WriteBoolean(RedmineKeys.MULTIPLE, Multiple); + } + #endregion + + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + if (Values == null) + { + return; + } + + var itemsCount = Values.Count; + Multiple = itemsCount > 1; + + writer.WriteStartObject(); + writer.WriteProperty(RedmineKeys.ID, Id); + writer.WriteProperty(RedmineKeys.NAME, Name); + writer.WriteBoolean(RedmineKeys.MULTIPLE, Multiple); + + if (Multiple) + { + writer.WritePropertyName(RedmineKeys.VALUE); + writer.WriteStartArray(); + foreach (var cfv in Values) + { + writer.WriteValue(cfv.Info); + } + writer.WriteEndArray(); + } + else + { + writer.WriteProperty(RedmineKeys.VALUE, itemsCount > 0 ? Values[0].Info : null); + } + + writer.WriteEndObject(); + } + + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.MULTIPLE: Multiple = reader.ReadAsBool(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.VALUE: + reader.Read(); + switch (reader.TokenType) + { + case JsonToken.Null: break; + case JsonToken.StartArray: + Values = reader.ReadAsCollection(); + break; + default: + Values = new List { new CustomFieldValue { Info = reader.Value as string } }; + break; + } + break; + } + } + } + + #endregion + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public bool Equals(IssueCustomField other) + { + if (other == null) return false; + return base.Equals(other) + && Multiple == other.Multiple + && (Values?.Equals(other.Values) ?? other.Values == null); + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as IssueCustomField); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Values, hashCode); + hashCode = HashCodeHelper.GetHashCode(Multiple, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(IssueCustomField left, IssueCustomField right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(IssueCustomField left, IssueCustomField right) + { + return !Equals(left, right); + } + #endregion + + #region Implementation of IClonable + /// + /// + /// + /// + public new IssueCustomField Clone(bool resetId) + { + IssueCustomField clone; + if (resetId) + { + clone = new IssueCustomField(); + } + else + { + clone = new IssueCustomField + { + Id = Id, + }; + } + + clone.Name = Name; + clone.Multiple = Multiple; + + if (Values != null) + { + clone.Values = new List(Values); + } + + return clone; + } + + #endregion + + #region Implementation of IValue + /// + /// + /// + public string Value => Id.ToInvariantString(); + + #endregion + + /// + /// + /// + /// + /// + public static string GetValue(object item) + { + return ((CustomFieldValue)item).Info; + } + + /// + /// + /// + /// + private string DebuggerDisplay => $"[IssueCustomField: Id={Id.ToInvariantString()}, Name={Name}, Multiple={Multiple.ToInvariantString()}]"; + + /// + /// + /// + /// + /// + /// + /// + public static IssueCustomField CreateSingle(int id, string name, string value) + { + return new IssueCustomField + { + Id = id, + Name = name, + Values = [new CustomFieldValue { Info = value }] + }; + } + + /// + /// + /// + /// + /// + /// + /// + public static IssueCustomField CreateMultiple(int id, string name, string[] values) + { + var isf = new IssueCustomField + { + Id = id, + Name = name, + Multiple = true, + }; + + if (values is not { Length: > 0 }) + { + return isf; + } + + isf.Values = new List(values.Length); + + foreach (var value in values) + { + isf.Values.Add(new CustomFieldValue { Info = value }); + } + + return isf; + } + } +} \ No newline at end of file diff --git a/redmine-net20-api/Types/IssuePriority.cs b/src/redmine-net-api/Types/IssuePriority.cs old mode 100755 new mode 100644 similarity index 50% rename from redmine-net20-api/Types/IssuePriority.cs rename to src/redmine-net-api/Types/IssuePriority.cs index cd33e2eb..914ffacd --- a/redmine-net20-api/Types/IssuePriority.cs +++ b/src/redmine-net-api/Types/IssuePriority.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2017 Adrian Popescu. + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,30 +15,42 @@ limitations under the License. */ using System; +using System.Diagnostics; using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; namespace Redmine.Net.Api.Types { /// /// Availability 2.2 /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.ISSUE_PRIORITY)] - public class IssuePriority : IdentifiableName, IEquatable + public sealed class IssuePriority : + IdentifiableName + ,IEquatable { + #region Properties /// /// /// - [XmlElement(RedmineKeys.IS_DEFAULT)] - public bool IsDefault { get; set; } + public bool IsDefault { get; internal set; } + /// + /// + /// + public bool IsActive { get; internal set; } + #endregion #region Implementation of IXmlSerializable /// /// Generates an object from its XML representation. /// - /// The stream from which the object is deserialized. + /// The stream from which the object is deserialized. public override void ReadXml(XmlReader reader) { reader.Read(); @@ -53,23 +65,46 @@ public override void ReadXml(XmlReader reader) switch (reader.Name) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; - + case RedmineKeys.ACTIVE: IsActive = reader.ReadElementContentAsBoolean(); break; case RedmineKeys.IS_DEFAULT: IsDefault = reader.ReadElementContentAsBoolean(); break; - + case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; default: reader.Read(); break; } } } + #endregion + + #region Implementation of IJsonSerialization /// /// /// - /// - public override void WriteXml(XmlWriter writer) { } + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } - #endregion + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.ACTIVE: IsActive = reader.ReadAsBool(); break; + case RedmineKeys.IS_DEFAULT: IsDefault = reader.ReadAsBool(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + #endregion #region Implementation of IEquatable /// @@ -81,7 +116,9 @@ public bool Equals(IssuePriority other) { if (other == null) return false; - return Id == other.Id && Name == other.Name && IsDefault == other.IsDefault; + return base.Equals(other) + && IsDefault == other.IsDefault + && IsActive == other.IsActive; } /// @@ -103,25 +140,40 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); - hashCode = HashCodeHelper.GetHashCode(IsDefault, hashCode); - return hashCode; - } + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(IsDefault, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsActive, hashCode); + return hashCode; } /// /// /// + /// + /// /// - public override string ToString() + public static bool operator ==(IssuePriority left, IssuePriority right) { - return string.Format("[IssuePriority: Id={0}, Name={1}, IsDefault={2}]", Id, Name, IsDefault); + return Equals(left, right); } + /// + /// + /// + /// + /// + /// + public static bool operator !=(IssuePriority left, IssuePriority right) + { + return !Equals(left, right); + } #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[IssuePriority: Id={Id.ToInvariantString()},Name={Name}, IsDefault={IsDefault.ToInvariantString()},IsActive={IsActive.ToInvariantString()}]"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs new file mode 100644 index 00000000..9abbe720 --- /dev/null +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -0,0 +1,314 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// Availability 1.3 + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.RELATION)] + public sealed class IssueRelation : Identifiable, ICloneable + { + #region Properties + /// + /// Gets or sets the issue id. + /// + /// The issue id. + public int IssueId { get; internal set; } + + /// + /// Gets or sets the related issue id. + /// + /// The issue to id. + public int IssueToId { get; set; } + + /// + /// Gets or sets the type of relation. + /// + /// The type. + public IssueRelationType Type { get; set; } + + /// + /// Gets or sets the delay for a "precedes" or "follows" relation. + /// + /// The delay. + public int? Delay { get; set; } + #endregion + + #region Implementation of IXmlSerialization + /// + /// + /// + /// + public override void ReadXml(XmlReader reader) + { + if (!reader.IsEmptyElement) reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + if (reader.IsEmptyElement && reader.HasAttributes) + { + while (reader.MoveToNextAttribute()) + { + var attributeName = reader.Name; + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadAttributeAsInt(attributeName); break; + case RedmineKeys.DELAY: Delay = reader.ReadAttributeAsNullableInt(attributeName); break; + case RedmineKeys.ISSUE_ID: IssueId = reader.ReadAttributeAsInt(attributeName); break; + case RedmineKeys.ISSUE_TO_ID: IssueToId = reader.ReadAttributeAsInt(attributeName); break; + case RedmineKeys.RELATION_TYPE: Type = ReadIssueRelationType(reader.GetAttribute(attributeName)); break; + } + } + return; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.DELAY: Delay = reader.ReadElementContentAsNullableInt(); break; + case RedmineKeys.ISSUE_ID: IssueId = reader.ReadElementContentAsInt(); break; + case RedmineKeys.ISSUE_TO_ID: IssueToId = reader.ReadElementContentAsInt(); break; + case RedmineKeys.RELATION_TYPE: Type = ReadIssueRelationType(reader.ReadElementContentAsString()); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteXml(XmlWriter writer) + { + AssertValidIssueRelationType(); + + writer.WriteElementString(RedmineKeys.ISSUE_TO_ID, IssueToId.ToInvariantString()); + writer.WriteElementString(RedmineKeys.RELATION_TYPE, Type.ToLowerName()); + + if (Type == IssueRelationType.Precedes || Type == IssueRelationType.Follows) + { + writer.WriteValueOrEmpty(RedmineKeys.DELAY, Delay); + } + } + #endregion + + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + AssertValidIssueRelationType(); + + using (new JsonObject(writer, RedmineKeys.RELATION)) + { + writer.WriteProperty(RedmineKeys.ISSUE_TO_ID, IssueToId); + writer.WriteProperty(RedmineKeys.RELATION_TYPE, Type.ToLowerName()); + + if (Type == IssueRelationType.Precedes || Type == IssueRelationType.Follows) + { + writer.WriteValueOrEmpty(RedmineKeys.DELAY, Delay); + } + } + } + + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.DELAY: Delay = reader.ReadAsInt32(); break; + case RedmineKeys.ISSUE_ID: IssueId = reader.ReadAsInt(); break; + case RedmineKeys.ISSUE_TO_ID: IssueToId = reader.ReadAsInt(); break; + case RedmineKeys.RELATION_TYPE: Type = ReadIssueRelationType(reader.ReadAsString()); break; + } + } + } + + private void AssertValidIssueRelationType() + { + if (Type == IssueRelationType.Undefined) + { + throw new RedmineException($"The value `{nameof(IssueRelationType)}.`{nameof(IssueRelationType.Undefined)}` is not allowed to create relations!"); + } + } + + private static IssueRelationType ReadIssueRelationType(string value) + { + if (value.IsNullOrWhiteSpace()) + { + return IssueRelationType.Undefined; + } + + if (short.TryParse(value, out var enumId)) + { + return (IssueRelationType)enumId; + } + + if (RedmineKeys.COPIED_TO.Equals(value, StringComparison.OrdinalIgnoreCase)) + { + return IssueRelationType.CopiedTo; + } + + if (RedmineKeys.COPIED_FROM.Equals(value, StringComparison.OrdinalIgnoreCase)) + { + return IssueRelationType.CopiedFrom; + } + +#if NETFRAMEWORK + return (IssueRelationType)Enum.Parse(typeof(IssueRelationType), value, true); +#else + return Enum.Parse(value, true); +#endif + } + + #endregion + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public override bool Equals(IssueRelation other) + { + if (other == null) return false; + return Id == other.Id + && IssueId == other.IssueId + && IssueToId == other.IssueToId + && Type == other.Type + && Delay == other.Delay; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as IssueRelation); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(IssueId, hashCode); + hashCode = HashCodeHelper.GetHashCode(IssueToId, hashCode); + hashCode = HashCodeHelper.GetHashCode(Type, hashCode); + hashCode = HashCodeHelper.GetHashCode(Delay, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(IssueRelation left, IssueRelation right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(IssueRelation left, IssueRelation right) + { + return !Equals(left, right); + } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[IssueRelation: Id={Id.ToInvariantString()}, IssueId={IssueId.ToInvariantString()}, Type={Type:G}, Delay={Delay?.ToInvariantString()}]"; + + /// + /// + /// + /// + public new IssueRelation Clone(bool resetId) + { + if (resetId) + { + return new IssueRelation + { + IssueId = IssueId, + IssueToId = IssueToId, + Type = Type, + Delay = Delay + }; + } + return new IssueRelation + { + Id = Id, + IssueId = IssueId, + IssueToId = IssueToId, + Type = Type, + Delay = Delay + }; + } + } +} \ No newline at end of file diff --git a/redmine-net20-api/Types/IssueRelationType.cs b/src/redmine-net-api/Types/IssueRelationType.cs similarity index 63% rename from redmine-net20-api/Types/IssueRelationType.cs rename to src/redmine-net-api/Types/IssueRelationType.cs index a7d42514..01d06292 100644 --- a/redmine-net20-api/Types/IssueRelationType.cs +++ b/src/redmine-net-api/Types/IssueRelationType.cs @@ -1,5 +1,5 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. +/* + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,6 +14,8 @@ You may obtain a copy of the License at limitations under the License. */ +using System.Xml.Serialization; + namespace Redmine.Net.Api.Types { /// @@ -21,41 +23,58 @@ namespace Redmine.Net.Api.Types /// public enum IssueRelationType { + #pragma warning disable CS0618 // Use of internal enumeration value is allowed here to have a fallback + /// + /// Fallback value for deserialization purposes in case the deserialization fails. Do not use to create new relations! + /// + Undefined = 0, + #pragma warning restore CS0618 /// /// /// - relates = 1, + Relates = 1, + /// /// /// - duplicates, + Duplicates, + /// /// /// - duplicated, + Duplicated, + /// /// /// - blocks, + Blocks, + /// /// /// - blocked, + Blocked, + /// /// /// - precedes, + Precedes, + /// /// /// - follows, + Follows, + /// /// /// - copied_to, + + [XmlEnum(RedmineKeys.COPIED_TO)] + CopiedTo, + /// /// /// - copied_from + [XmlEnum(RedmineKeys.COPIED_FROM)] + CopiedFrom } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssueStatus.cs b/src/redmine-net-api/Types/IssueStatus.cs new file mode 100644 index 00000000..08c7553a --- /dev/null +++ b/src/redmine-net-api/Types/IssueStatus.cs @@ -0,0 +1,257 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// Availability 1.3 + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.ISSUE_STATUS)] + public sealed class IssueStatus : IdentifiableName, IEquatable, ICloneable + { + /// + /// + /// + public IssueStatus() + { + + } + + /// + /// + /// + /// + public IssueStatus(int id) + { + Id = id; + } + + /// + /// + /// + internal IssueStatus(int id, string name, bool isDefault = false, bool isClosed = false) + { + Id = id; + Name = name; + IsClosed = isClosed; + IsDefault = isDefault; + } + + internal IssueStatus(XmlReader reader) + { + Initialize(reader); + } + + internal IssueStatus(JsonReader reader) + { + Initialize(reader); + } + + private void Initialize(XmlReader reader) + { + ReadXml(reader); + } + + private void Initialize(JsonReader reader) + { + ReadJson(reader); + } + + #region Properties + /// + /// Gets or sets a value indicating whether IssueStatus is default. + /// + /// + /// true if IssueStatus is default; otherwise, false. + /// + public bool IsDefault { get; internal set; } + + /// + /// Gets or sets a value indicating whether IssueStatus is closed. + /// + /// true if IssueStatus is closed; otherwise, false. + public bool IsClosed { get; internal set; } + #endregion + + #region Implementation of IXmlSerialization + /// + /// + /// + /// + public override void ReadXml(XmlReader reader) + { + if (reader.HasAttributes && reader.Name == "status") + { + Id = reader.ReadAttributeAsInt(RedmineKeys.ID); + IsClosed = reader.ReadAttributeAsBoolean(RedmineKeys.IS_CLOSED); + IsDefault = reader.ReadAttributeAsBoolean(RedmineKeys.IS_DEFAULT); + Name = reader.GetAttribute(RedmineKeys.NAME); + reader.Read(); + return; + } + + reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.IS_CLOSED: IsClosed = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.IS_DEFAULT: IsDefault = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; + default: reader.Read(); break; + } + } + } + #endregion + + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.IS_CLOSED: IsClosed = reader.ReadAsBool(); break; + case RedmineKeys.IS_DEFAULT: IsDefault = reader.ReadAsBool(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + #endregion + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public bool Equals(IssueStatus other) + { + if (other == null) return false; + return base.Equals(other) + && IsClosed == other.IsClosed + && IsDefault == other.IsDefault; + } + + /// + /// + /// + /// + /// + public new IssueStatus Clone(bool resetId) + { + return new IssueStatus + { + Id = Id, + Name = Name, + IsClosed = IsClosed, + IsDefault = IsDefault + }; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as IssueStatus); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + unchecked + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(IsClosed, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsDefault, hashCode); + return hashCode; + } + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(IssueStatus left, IssueStatus right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(IssueStatus left, IssueStatus right) + { + return !Equals(left, right); + } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[IssueStatus: Id={Id.ToInvariantString()}, Name={Name}, IsDefault={IsDefault.ToInvariantString()}, IsClosed={IsClosed.ToInvariantString()}]"; + + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/Journal.cs b/src/redmine-net-api/Types/Journal.cs new file mode 100644 index 00000000..d461aac3 --- /dev/null +++ b/src/redmine-net-api/Types/Journal.cs @@ -0,0 +1,283 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.JOURNAL)] + public sealed class Journal : + Identifiable + ,ICloneable + { + #region Properties + /// + /// Gets the user. + /// + /// + /// The user. + /// + public IdentifiableName User { get; internal set; } + + /// + /// Gets or sets the notes. + /// + /// + /// The notes. + /// + /// Setting Notes to string.empty or null will destroy the journal + /// + public string Notes { get; set; } + + /// + /// Gets the created on. + /// + /// + /// The created on. + /// + public DateTime? CreatedOn { get; internal set; } + + /// + /// Gets the updated on. + /// + /// + /// The updated on. + /// + public DateTime? UpdatedOn { get; internal set; } + + /// + /// + /// + public bool PrivateNotes { get; internal set; } + + /// + /// Gets the details. + /// + /// + /// The details. + /// + public List Details { get; internal set; } + + /// + /// + /// + public IdentifiableName UpdatedBy { get; internal set; } + #endregion + + #region Implementation of IXmlSerialization + /// + /// + /// + /// + public override void ReadXml(XmlReader reader) + { + Id = reader.ReadAttributeAsInt(RedmineKeys.ID); + reader.Read(); + + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.DETAILS: Details = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.NOTES: Notes = reader.ReadElementContentAsString(); break; + case RedmineKeys.PRIVATE_NOTES: PrivateNotes = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.USER: User = new IdentifiableName(reader); break; + case RedmineKeys.UPDATED_BY: UpdatedBy = new IdentifiableName(reader); break; + default: reader.Read(); break; + } + } + } + + /// + public override void WriteXml(XmlWriter writer) + { + writer.WriteElementString(RedmineKeys.NOTES, Notes); + } + + #endregion + + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.DETAILS: Details = reader.ReadAsCollection(); break; + case RedmineKeys.NOTES: Notes = reader.ReadAsString(); break; + case RedmineKeys.PRIVATE_NOTES: PrivateNotes = reader.ReadAsBool(); break; + case RedmineKeys.USER: User = new IdentifiableName(reader); break; + case RedmineKeys.UPDATED_BY: UpdatedBy = new IdentifiableName(reader); break; + default: reader.Read(); break; + } + } + } + + /// + public override void WriteJson(JsonWriter writer) + { + writer.WriteProperty(RedmineKeys.NOTES, Notes); + } + + #endregion + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public override bool Equals(Journal other) + { + if (other == null) return false; + var result = base.Equals(other); + result = result && User == other.User; + result = result && UpdatedBy == other.UpdatedBy; + result = result && (Details?.Equals(other.Details) ?? other.Details == null); + result = result && string.Equals(Notes, other.Notes, StringComparison.Ordinal); + result = result && CreatedOn == other.CreatedOn; + result = result && UpdatedOn == other.UpdatedOn; + result = result && PrivateNotes == other.PrivateNotes; + return result; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as Journal); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(User, hashCode); + hashCode = HashCodeHelper.GetHashCode(Notes, hashCode); + hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(Details, hashCode); + hashCode = HashCodeHelper.GetHashCode(PrivateNotes, hashCode); + hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(UpdatedBy, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Journal left, Journal right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Journal left, Journal right) + { + return !Equals(left, right); + } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[Journal: Id={Id.ToInvariantString()}, CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}]"; + + /// + /// + /// + /// + public new Journal Clone(bool resetId) + { + if (resetId) + { + return new Journal + { + User = User?.Clone(false), + Notes = Notes, + CreatedOn = CreatedOn, + PrivateNotes = PrivateNotes, + Details = Details?.Clone(false) + }; + } + return new Journal + { + Id = Id, + User = User?.Clone(false), + Notes = Notes, + CreatedOn = CreatedOn, + PrivateNotes = PrivateNotes, + Details = Details?.Clone(false) + }; + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/Membership.cs b/src/redmine-net-api/Types/Membership.cs new file mode 100644 index 00000000..d6831787 --- /dev/null +++ b/src/redmine-net-api/Types/Membership.cs @@ -0,0 +1,190 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System.Collections.Generic; +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// Only the roles can be updated, the project and the user of a membership are read-only. + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.MEMBERSHIP)] + public sealed class Membership : Identifiable + { + #region Properties + /// + /// Gets the group. + /// + public IdentifiableName Group { get; internal set; } + /// + /// Gets or sets the project. + /// + /// The project. + public IdentifiableName Project { get; internal set; } + + /// + /// Gets the user. + /// + public IdentifiableName User { get; internal set; } + + /// + /// Gets or sets the type. + /// + /// The type. + public List Roles { get; internal set; } + #endregion + + #region Implementation of IXmlSerialization + /// + /// + /// + /// + public override void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.GROUP: Group = new IdentifiableName(reader); break; + case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; + case RedmineKeys.USER: User = new IdentifiableName(reader); break; + case RedmineKeys.ROLES: Roles = reader.ReadElementContentAsCollection(); break; + default: reader.Read(); break; + } + } + } + #endregion + + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.GROUP: Project = new IdentifiableName(reader); break; + case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; + case RedmineKeys.USER: Project = new IdentifiableName(reader); break; + case RedmineKeys.ROLES: Roles = reader.ReadAsCollection(); break; + default: reader.Read(); break; + } + } + } + #endregion + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public override bool Equals(Membership other) + { + if (other == null) return false; + return Id == other.Id + && Project == other.Project + && Roles != null ? Roles.Equals(other.Roles) : other.Roles == null; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as Membership); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Group, hashCode); + hashCode = HashCodeHelper.GetHashCode(Project, hashCode); + hashCode = HashCodeHelper.GetHashCode(User, hashCode); + hashCode = HashCodeHelper.GetHashCode(Roles, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Membership left, Membership right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Membership left, Membership right) + { + return !Equals(left, right); + } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[Membership: Id={Id.ToInvariantString()}]"; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/MembershipRole.cs b/src/redmine-net-api/Types/MembershipRole.cs new file mode 100644 index 00000000..7e150a9f --- /dev/null +++ b/src/redmine-net-api/Types/MembershipRole.cs @@ -0,0 +1,183 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Common; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.ROLE)] + public sealed class MembershipRole : IdentifiableName, IEquatable, IValue + { + #region Properties + /// + /// Gets or sets a value indicating whether this is inherited. + /// + /// + /// true if inherited; otherwise, false. + /// + public bool Inherited { get; internal set; } + #endregion + + #region Implementation of IXmlSerialization + /// + /// Reads the XML. + /// + /// The reader. + public override void ReadXml(XmlReader reader) + { + Id = reader.ReadAttributeAsInt(RedmineKeys.ID); + Inherited = reader.ReadAttributeAsBoolean(RedmineKeys.INHERITED); + Name = reader.GetAttribute(RedmineKeys.NAME); + reader.Read(); + } + + /// + /// + /// + /// + public override void WriteXml(XmlWriter writer) + { + writer.WriteValue(Id); + } + #endregion + + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.INHERITED: Inherited = reader.ReadAsBool(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + writer.WriteProperty(RedmineKeys.ID, Id.ToInvariantString()); + } + + #endregion + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public bool Equals(MembershipRole other) + { + if (other == null) return false; + return base.Equals(other) + && Inherited == other.Inherited; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as MembershipRole); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Inherited, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(MembershipRole left, MembershipRole right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(MembershipRole left, MembershipRole right) + { + return !Equals(left, right); + } + #endregion + + #region Implementation of IClonable + /// + /// + /// + public string Value => Id.ToInvariantString(); + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[MembershipRole: Id={Id.ToInvariantString()}, Name={Name}, Inherited={Inherited.ToInvariantString()}]"; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/MyAccount.cs b/src/redmine-net-api/Types/MyAccount.cs new file mode 100644 index 00000000..754e03f0 --- /dev/null +++ b/src/redmine-net-api/Types/MyAccount.cs @@ -0,0 +1,255 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + /// Availability 4.1 + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.USER)] + public sealed class MyAccount : Identifiable + { + #region Properties + + /// + /// Gets the user login. + /// + /// The login. + public string Login { get; internal set; } + + /// + /// Gets the first name. + /// + /// The first name. + public string FirstName { get; internal set; } + + /// + /// Gets the last name. + /// + /// The last name. + public string LastName { get; internal set; } + + /// + /// Gets the email. + /// + /// The email. + public string Email { get; internal set; } + + /// + /// Returns true if user is admin. + /// + /// + /// The authentication mode id. + /// + public bool IsAdmin { get; internal set; } + + /// + /// Gets the created on. + /// + /// The created on. + public DateTime? CreatedOn { get; internal set; } + + /// + /// Gets the last login on. + /// + /// The last login on. + public DateTime? LastLoginOn { get; internal set; } + + /// + /// Gets the API key + /// + public string ApiKey { get; internal set; } + + /// + /// Gets or sets the custom fields + /// + public List CustomFields { get; set; } + + #endregion + + #region Implementation of IXmlSerializable + + /// + public override void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.ADMIN: IsAdmin = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.API_KEY: ApiKey = reader.ReadElementContentAsString(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.FIRST_NAME: FirstName = reader.ReadElementContentAsString(); break; + case RedmineKeys.LAST_LOGIN_ON: LastLoginOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.LAST_NAME: LastName = reader.ReadElementContentAsString(); break; + case RedmineKeys.LOGIN: Login = reader.ReadElementContentAsString(); break; + case RedmineKeys.MAIL: Email = reader.ReadElementContentAsString(); break; + default: reader.Read(); break; + } + } + } + + /// + public override void WriteXml(XmlWriter writer) + { + writer.WriteElementString(RedmineKeys.FIRST_NAME, FirstName); + writer.WriteElementString(RedmineKeys.LAST_NAME, LastName); + writer.WriteElementString(RedmineKeys.MAIL, Email); + } + + #endregion + + #region Implementation of IJsonSerializable + + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.ADMIN: IsAdmin = reader.ReadAsBool(); break; + case RedmineKeys.API_KEY: ApiKey = reader.ReadAsString(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadAsCollection(); break; + case RedmineKeys.FIRST_NAME: FirstName = reader.ReadAsString(); break; + case RedmineKeys.LAST_LOGIN_ON: LastLoginOn = reader.ReadAsDateTime(); break; + case RedmineKeys.LAST_NAME: LastName = reader.ReadAsString(); break; + case RedmineKeys.LOGIN: Login = reader.ReadAsString(); break; + case RedmineKeys.MAIL: Email = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + + /// + public override void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.USER)) + { + writer.WriteProperty(RedmineKeys.FIRST_NAME, FirstName); + writer.WriteProperty(RedmineKeys.LAST_NAME, LastName); + writer.WriteProperty(RedmineKeys.MAIL, Email); + } + } + + #endregion + + /// + public override bool Equals(MyAccount other) + { + if (other == null) return false; + return Id == other.Id + && string.Equals(Login, other.Login, StringComparison.Ordinal) + && string.Equals(FirstName, other.FirstName, StringComparison.Ordinal) + && string.Equals(LastName, other.LastName, StringComparison.Ordinal) + && string.Equals(ApiKey, other.ApiKey, StringComparison.Ordinal) + && string.Equals(Email, other.Email, StringComparison.Ordinal) + && IsAdmin == other.IsAdmin + && CreatedOn == other.CreatedOn + && LastLoginOn == other.LastLoginOn + && (CustomFields?.Equals(other.CustomFields) ?? other.CustomFields == null); + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as MyAccount); + } + + /// + public override int GetHashCode() + { + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Id, hashCode); + hashCode = HashCodeHelper.GetHashCode(Login, hashCode); + hashCode = HashCodeHelper.GetHashCode(FirstName, hashCode); + hashCode = HashCodeHelper.GetHashCode(LastName, hashCode); + hashCode = HashCodeHelper.GetHashCode(ApiKey, hashCode); + hashCode = HashCodeHelper.GetHashCode(Email, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsAdmin, hashCode); + hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(LastLoginOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(MyAccount left, MyAccount right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(MyAccount left, MyAccount right) + { + return !Equals(left, right); + } + + private string DebuggerDisplay => $"[MyAccount: Id={Id.ToInvariantString()}, Login={Login}, IsAdmin={IsAdmin.ToInvariantString()}]"; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/MyAccountCustomField.cs b/src/redmine-net-api/Types/MyAccountCustomField.cs new file mode 100644 index 00000000..4abf9723 --- /dev/null +++ b/src/redmine-net-api/Types/MyAccountCustomField.cs @@ -0,0 +1,172 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.CUSTOM_FIELD)] + public sealed class MyAccountCustomField : IdentifiableName, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// Serialization + public MyAccountCustomField() { } + + /// + /// + /// + public string Value { get; internal set; } + + internal MyAccountCustomField(int id, string name) + { + Id = id; + Name = name; + } + + /// + public override void ReadXml(XmlReader reader) + { + base.ReadXml(reader); + while (!reader.EOF) + { + if (reader.IsEmptyElement) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.VALUE: + Value = reader.ReadElementContentAsString(); + break; + + default: + reader.Read(); + break; + } + } + } + + /// + public override void WriteXml(XmlWriter writer) + { + } + + + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType == JsonToken.PropertyName) + { + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.VALUE: Value = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + } + + /// + public override void WriteJson(JsonWriter writer) + { + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj is MyAccountCustomField other && Equals(other); + } + + /// + /// + /// + /// + /// + public bool Equals(MyAccountCustomField other) + { + if (other == null) return false; + return base.Equals(other) + && string.Equals(Value, other.Value, StringComparison.Ordinal); + } + + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Value, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(MyAccountCustomField left, MyAccountCustomField right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(MyAccountCustomField left, MyAccountCustomField right) + { + return !Equals(left, right); + } + + /// + /// + /// + /// + private string DebuggerDisplay => $"[MyAccountCustomField: Id={Id.ToInvariantString()}, Name={Name}, Value: {Value}]"; + + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/News.cs b/src/redmine-net-api/Types/News.cs new file mode 100644 index 00000000..b31e519c --- /dev/null +++ b/src/redmine-net-api/Types/News.cs @@ -0,0 +1,284 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// Availability 1.1 + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.NEWS)] + public sealed class News : Identifiable + { + #region Properties + /// + /// Gets or sets the project. + /// + /// The project. + public IdentifiableName Project { get; internal set; } + + /// + /// Gets or sets the author. + /// + /// The author. + public IdentifiableName Author { get; internal set; } + + /// + /// Gets or sets the title. + /// + /// The title. + public string Title { get; internal set; } + + /// + /// Gets or sets the summary. + /// + /// The summary. + public string Summary { get; internal set; } + + /// + /// Gets or sets the description. + /// + /// The description. + public string Description { get; internal set; } + + /// + /// Gets or sets the created on. + /// + /// The created on. + public DateTime? CreatedOn { get; internal set; } + + /// + /// + /// + public List Attachments { get; internal set; } + + /// + /// + /// + public List Comments { get; internal set; } + + /// + /// + /// + public List Uploads { get; set; } + + #endregion + + #region Implementation of IXmlSerialization + /// + /// + /// + /// + public override void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; + case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; + case RedmineKeys.SUMMARY: Summary = reader.ReadElementContentAsString(); break; + case RedmineKeys.TITLE: Title = reader.ReadElementContentAsString(); break; + case RedmineKeys.ATTACHMENTS: Attachments = reader.ReadElementContentAsCollection(); + break; + case RedmineKeys.COMMENTS: Comments = reader.ReadElementContentAsCollection(); + break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteXml(XmlWriter writer) + { + writer.WriteElementString(RedmineKeys.TITLE, Title); + writer.WriteElementString(RedmineKeys.SUMMARY, Summary); + writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); + if (Uploads != null) + { + writer.WriteArray(RedmineKeys.UPLOADS, Uploads); + } + } + + #endregion + + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadAsString(); break; + case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; + case RedmineKeys.SUMMARY: Summary = reader.ReadAsString(); break; + case RedmineKeys.TITLE: Title = reader.ReadAsString(); break; + case RedmineKeys.ATTACHMENTS: Attachments = reader.ReadAsCollection(); break; + case RedmineKeys.COMMENTS: Comments = reader.ReadAsCollection(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.NEWS)) + { + writer.WriteProperty(RedmineKeys.TITLE, Title); + writer.WriteProperty(RedmineKeys.SUMMARY, Summary); + writer.WriteProperty(RedmineKeys.DESCRIPTION, Description); + if (Uploads != null) + { + writer.WriteArray(RedmineKeys.UPLOADS, Uploads); + } + } + } + + #endregion + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public override bool Equals(News other) + { + if(other == null) return false; + + var result = base.Equals(other); + result = result && Project == other.Project; + result = result && Author == other.Author; + result = result && string.Equals(Title, other.Title, StringComparison.Ordinal); + result = result && string.Equals(Summary, other.Summary, StringComparison.Ordinal); + result = result && string.Equals(Description, other.Description, StringComparison.Ordinal); + result = result && CreatedOn == other.CreatedOn; + result = result && (Attachments?.Equals(other.Attachments) ?? other.Attachments == null); + result = result && (Comments?.Equals(other.Comments) ?? other.Comments == null); + result = result && (Uploads?.Equals(other.Uploads) ?? other.Uploads == null); + return result; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as News); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Id, hashCode); + hashCode = HashCodeHelper.GetHashCode(Project, hashCode); + hashCode = HashCodeHelper.GetHashCode(Author, hashCode); + hashCode = HashCodeHelper.GetHashCode(Title, hashCode); + hashCode = HashCodeHelper.GetHashCode(Summary, hashCode); + hashCode = HashCodeHelper.GetHashCode(Description, hashCode); + hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(Comments, hashCode); + hashCode = HashCodeHelper.GetHashCode(Attachments, hashCode); + hashCode = HashCodeHelper.GetHashCode(Uploads, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(News left, News right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(News left, News right) + { + return !Equals(left, right); + } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[News: Id={Id.ToInvariantString()}, Title={Title}]"; + + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/NewsComment.cs b/src/redmine-net-api/Types/NewsComment.cs new file mode 100644 index 00000000..aaee7ae9 --- /dev/null +++ b/src/redmine-net-api/Types/NewsComment.cs @@ -0,0 +1,160 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.COMMENT)] + public sealed class NewsComment: Identifiable + { + /// + /// + /// + public IdentifiableName Author { get; set; } + /// + /// + /// + public string Content { get; set; } + + /// + public override void ReadXml(XmlReader reader) + { + reader.Read(); + + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; + case RedmineKeys.CONTENT: Content = reader.ReadElementContentAsString(); break; + default: reader.Read(); break; + } + } + } + + /// + public override void WriteXml(XmlWriter writer) + { + } + + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: + Id = reader.ReadAsInt32().GetValueOrDefault(); + break; + case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; + case RedmineKeys.CONTENT: Content = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + + /// + public override void WriteJson(JsonWriter writer) + { + } + + /// + public override bool Equals(NewsComment other) + { + if (other == null) return false; + return Id == other.Id + && Author == other.Author + && string.Equals(Content, other.Content, StringComparison.Ordinal); + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as NewsComment); + } + + /// + public override int GetHashCode() + { + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Id, hashCode); + hashCode = HashCodeHelper.GetHashCode(Author, hashCode); + hashCode = HashCodeHelper.GetHashCode(Content, hashCode); + + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(NewsComment left, NewsComment right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(NewsComment left, NewsComment right) + { + return !Equals(left, right); + } + + private string DebuggerDisplay => $"[NewsComment: Id={Id.ToInvariantString()}]"; + } +} \ No newline at end of file diff --git a/redmine-net20-api/Types/ChangeSet.cs b/src/redmine-net-api/Types/Permission.cs old mode 100755 new mode 100644 similarity index 51% rename from redmine-net20-api/Types/ChangeSet.cs rename to src/redmine-net-api/Types/Permission.cs index 414a4216..bf411f5e --- a/redmine-net20-api/Types/ChangeSet.cs +++ b/src/redmine-net-api/Types/Permission.cs @@ -1,5 +1,5 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. +/* + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,77 +15,71 @@ limitations under the License. */ using System; +using System.Diagnostics; using System.Xml; using System.Xml.Schema; using System.Xml.Serialization; -using Redmine.Net.Api.Extensions; +using Newtonsoft.Json; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json; namespace Redmine.Net.Api.Types { /// /// /// - [XmlRoot(RedmineKeys.CHANGESET)] - public class ChangeSet : IXmlSerializable, IEquatable + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.PERMISSION)] + #pragma warning disable CA1711 + public sealed class Permission : IXmlSerializable, IJsonSerializable, IEquatable + #pragma warning restore CA1711 { + #region Properties /// /// /// - [XmlAttribute(RedmineKeys.REVISION)] - public int Revision { get; set; } + public string Info { get; internal set; } + #endregion - /// - /// - /// - [XmlElement(RedmineKeys.USER)] - public IdentifiableName User { get; set; } + #region Implementation of IXmlSerializable /// /// /// - [XmlElement(RedmineKeys.COMMENTS)] - public string Comments { get; set; } + /// + public XmlSchema GetSchema() { return null; } /// /// /// - [XmlElement(RedmineKeys.COMMITTED_ON, IsNullable = true)] - public DateTime? CommittedOn { get; set; } + /// + public void ReadXml(XmlReader reader) + { + reader.Read(); + if (reader.NodeType == XmlNodeType.Text) + { + Info = reader.Value; + } + } /// /// /// - /// - public XmlSchema GetSchema() { return null; } + /// + public void WriteXml(XmlWriter writer) { } + + #endregion + #region Implementation of IJsonSerialization /// /// /// /// - public void ReadXml(XmlReader reader) + public void ReadJson(JsonReader reader) { - reader.Read(); - while (!reader.EOF) + if (reader.TokenType == JsonToken.String) { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - Revision = reader.ReadAttributeAsInt(RedmineKeys.REVISION); - - switch (reader.Name) - { - case RedmineKeys.USER: User = new IdentifiableName(reader); break; - - case RedmineKeys.COMMENTS: Comments = reader.ReadElementContentAsString(); break; - - case RedmineKeys.COMMITTED_ON: CommittedOn = reader.ReadElementContentAsNullableDateTime(); break; - - default: reader.Read(); break; - } + Info = reader.Value as string; } } @@ -93,21 +87,18 @@ public void ReadXml(XmlReader reader) /// /// /// - public void WriteXml(XmlWriter writer) { } + public void WriteJson(JsonWriter writer) { } + #endregion + #region Implementation of IEquatable /// /// /// /// /// - public bool Equals(ChangeSet other) + public bool Equals(Permission other) { - if (other == null) return false; - - return Revision == other.Revision - && User == other.User - && Comments == other.Comments - && CommittedOn == other.CommittedOn; + return other != null && string.Equals(Info, other.Info, StringComparison.Ordinal); } /// @@ -120,7 +111,7 @@ public override bool Equals(object obj) if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != GetType()) return false; - return Equals(obj as ChangeSet); + return Equals(obj as Permission); } /// @@ -129,24 +120,39 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Revision, hashCode); - hashCode = HashCodeHelper.GetHashCode(User, hashCode); - hashCode = HashCodeHelper.GetHashCode(Comments, hashCode); - hashCode = HashCodeHelper.GetHashCode(CommittedOn, hashCode); - return hashCode; - } + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Info, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Permission left, Permission right) + { + return Equals(left, right); } /// /// /// + /// + /// /// - public override string ToString() + public static bool operator !=(Permission left, Permission right) { - return string.Format("Revision: {0}, User: '{1}', CommitedOn: {2}, Comments: '{3}'", Revision, User, CommittedOn, Comments); + return !Equals(left, right); } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[Permission: {Info}]"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs new file mode 100644 index 00000000..c993461a --- /dev/null +++ b/src/redmine-net-api/Types/Project.cs @@ -0,0 +1,406 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Common; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// Availability 1.0 + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.PROJECT)] + public sealed class Project : IdentifiableName, IEquatable + { + #region Properties + + /// + /// Gets or sets the identifier. + /// + /// Required for create + /// The identifier. + public string Identifier { get; set; } + + /// + /// Gets or sets the description. + /// + /// The description. + public string Description { get; set; } + + /// + /// Gets or sets the parent. + /// + /// The parent. + public IdentifiableName Parent { get; set; } + + /// + /// Gets or sets the home page. + /// + /// The home page. + public string HomePage { get; set; } + + /// + /// Gets the created on. + /// + /// The created on. + public DateTime? CreatedOn { get; internal set; } + + /// + /// Gets the updated on. + /// + /// The updated on. + public DateTime? UpdatedOn { get; internal set; } + + /// + /// Gets the status. + /// + /// + /// The status. + /// + public ProjectStatus Status { get; internal set; } + + /// + /// Gets or sets a value indicating whether this project is public. + /// + /// + /// true if this project is public; otherwise, false. + /// + /// Available in Redmine starting with 2.6.0 version. + public bool IsPublic { get; set; } + + /// + /// Gets or sets a value indicating whether [inherit members]. + /// + /// + /// true if [inherit members]; otherwise, false. + /// + public bool InheritMembers { get; set; } + + /// + /// Gets or sets the trackers. + /// + /// + /// The trackers. + /// + /// Available in Redmine starting with 2.6.0 version. + public List Trackers { get; set; } + + /// + /// Gets or sets the enabled modules. + /// + /// + /// The enabled modules. + /// + /// Available in Redmine starting with 2.6.0 version. + public List EnabledModules { get; set; } + + /// + /// + /// + public List IssueCustomFields { get; set; } + + /// + /// + /// + public List CustomFieldValues { get; set; } + + /// + /// Gets the issue categories. + /// + /// + /// The issue categories. + /// + /// Available in Redmine starting with the 2.6.0 version. + public List IssueCategories { get; internal set; } + + /// + /// Gets the time entry activities. + /// + /// Available in Redmine starting with the 3.4.0 version. + public List TimeEntryActivities { get; internal set; } + + /// + /// + /// + public IdentifiableName DefaultVersion { get; set; } + + /// + /// + /// + public IdentifiableName DefaultAssignee { get; set; } + #endregion + + #region Implementation of IXmlSerializer + /// + /// Generates an object from its XML representation. + /// + /// The stream from which the object is deserialized. + public override void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.CUSTOM_FIELDS: IssueCustomFields = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; + case RedmineKeys.ENABLED_MODULES: EnabledModules = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.HOMEPAGE: HomePage = reader.ReadElementContentAsString(); break; + case RedmineKeys.IDENTIFIER: Identifier = reader.ReadElementContentAsString(); break; + case RedmineKeys.INHERIT_MEMBERS: InheritMembers = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.IS_PUBLIC: IsPublic = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.ISSUE_CATEGORIES: IssueCategories = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; + case RedmineKeys.PARENT: Parent = new IdentifiableName(reader); break; + case RedmineKeys.STATUS: Status = (ProjectStatus)reader.ReadElementContentAsInt(); break; + case RedmineKeys.TIME_ENTRY_ACTIVITIES: TimeEntryActivities = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.TRACKERS: Trackers = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.DEFAULT_ASSIGNEE: DefaultAssignee = new IdentifiableName(reader); break; + case RedmineKeys.DEFAULT_VERSION: DefaultVersion = new IdentifiableName(reader); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + public override void WriteXml(XmlWriter writer) + { + writer.WriteElementString(RedmineKeys.NAME, Name); + writer.WriteElementString(RedmineKeys.IDENTIFIER, Identifier); + writer.WriteIfNotDefaultOrNull(RedmineKeys.DESCRIPTION, Description); + writer.WriteIfNotDefaultOrNull(RedmineKeys.HOMEPAGE, HomePage); + writer.WriteBoolean(RedmineKeys.IS_PUBLIC, IsPublic); + writer.WriteIdIfNotNull(RedmineKeys.PARENT_ID, Parent); + writer.WriteBoolean(RedmineKeys.INHERIT_MEMBERS, InheritMembers); + + //It works only when the new project is a subproject, and it inherits the members. + writer.WriteIdIfNotNull(RedmineKeys.DEFAULT_ASSIGNED_TO_ID, DefaultAssignee); + //It works only with existing shared versions. + writer.WriteIdIfNotNull(RedmineKeys.DEFAULT_VERSION_ID, DefaultVersion); + + writer.WriteRepeatableElement(RedmineKeys.TRACKER_IDS, (IEnumerable)Trackers); + writer.WriteRepeatableElement(RedmineKeys.ENABLED_MODULE_NAMES, (IEnumerable)EnabledModules); + writer.WriteRepeatableElement(RedmineKeys.ISSUE_CUSTOM_FIELD_IDS, (IEnumerable)IssueCustomFields); + if (Id == 0) + { + writer.WriteArray(RedmineKeys.CUSTOM_FIELD_VALUES, CustomFieldValues); + } + } + #endregion + + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.CUSTOM_FIELDS: IssueCustomFields = reader.ReadAsCollection(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadAsString(); break; + case RedmineKeys.ENABLED_MODULES: EnabledModules = reader.ReadAsCollection(); break; + case RedmineKeys.HOMEPAGE: HomePage = reader.ReadAsString(); break; + case RedmineKeys.IDENTIFIER: Identifier = reader.ReadAsString(); break; + case RedmineKeys.INHERIT_MEMBERS: InheritMembers = reader.ReadAsBool(); break; + case RedmineKeys.IS_PUBLIC: IsPublic = reader.ReadAsBool(); break; + case RedmineKeys.ISSUE_CATEGORIES: IssueCategories = reader.ReadAsCollection(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.PARENT: Parent = new IdentifiableName(reader); break; + case RedmineKeys.STATUS: Status = (ProjectStatus)reader.ReadAsInt(); break; + case RedmineKeys.TIME_ENTRY_ACTIVITIES: TimeEntryActivities = reader.ReadAsCollection(); break; + case RedmineKeys.TRACKERS: Trackers = reader.ReadAsCollection(); break; + case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.DEFAULT_ASSIGNEE: DefaultAssignee = new IdentifiableName(reader); break; + case RedmineKeys.DEFAULT_VERSION: DefaultVersion = new IdentifiableName(reader); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.PROJECT)) + { + writer.WriteProperty(RedmineKeys.NAME, Name); + writer.WriteProperty(RedmineKeys.IDENTIFIER, Identifier); + writer.WriteIfNotDefaultOrNull(RedmineKeys.DESCRIPTION, Description); + writer.WriteIfNotDefaultOrNull(RedmineKeys.HOMEPAGE, HomePage); + writer.WriteBoolean(RedmineKeys.INHERIT_MEMBERS, InheritMembers); + writer.WriteBoolean(RedmineKeys.IS_PUBLIC, IsPublic); + writer.WriteIdIfNotNull(RedmineKeys.PARENT_ID, Parent); + + //It works only when the new project is a subproject, and it inherits the members. + writer.WriteIdIfNotNull(RedmineKeys.DEFAULT_ASSIGNED_TO_ID, DefaultAssignee); + //It works only with existing shared versions. + writer.WriteIdIfNotNull(RedmineKeys.DEFAULT_VERSION_ID, DefaultVersion); + + writer.WriteRepeatableElement(RedmineKeys.TRACKER_IDS, (IEnumerable)Trackers); + writer.WriteRepeatableElement(RedmineKeys.ENABLED_MODULE_NAMES, (IEnumerable)EnabledModules); + writer.WriteRepeatableElement(RedmineKeys.ISSUE_CUSTOM_FIELD_IDS, (IEnumerable)IssueCustomFields); + if (Id == 0) + { + writer.WriteArray(RedmineKeys.CUSTOM_FIELD_VALUES, CustomFieldValues); + } + } + } + #endregion + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public bool Equals(Project other) + { + if (other == null) + { + return false; + } + + return base.Equals(other) + && string.Equals(Identifier, other.Identifier, StringComparison.Ordinal) + && string.Equals(Description, other.Description, StringComparison.Ordinal) + && string.Equals(HomePage, other.HomePage, StringComparison.Ordinal) + && string.Equals(Identifier, other.Identifier, StringComparison.Ordinal) + && CreatedOn == other.CreatedOn + && UpdatedOn == other.UpdatedOn + && Status == other.Status + && IsPublic == other.IsPublic + && InheritMembers == other.InheritMembers + && DefaultAssignee == other.DefaultAssignee + && DefaultVersion == other.DefaultVersion + && Parent == other.Parent + && (Trackers?.Equals(other.Trackers) ?? other.Trackers == null) + && (IssueCustomFields?.Equals(other.IssueCustomFields) ?? other.IssueCustomFields == null) + && (CustomFieldValues?.Equals(other.CustomFieldValues) ?? other.CustomFieldValues == null) + && (IssueCategories?.Equals(other.IssueCategories) ?? other.IssueCategories == null) + && (EnabledModules?.Equals(other.EnabledModules) ?? other.EnabledModules == null) + && (TimeEntryActivities?.Equals(other.TimeEntryActivities) ?? other.TimeEntryActivities == null); + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as Project); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Identifier, hashCode); + hashCode = HashCodeHelper.GetHashCode(Description, hashCode); + hashCode = HashCodeHelper.GetHashCode(Parent, hashCode); + hashCode = HashCodeHelper.GetHashCode(HomePage, hashCode); + hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(Status, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsPublic, hashCode); + hashCode = HashCodeHelper.GetHashCode(InheritMembers, hashCode); + hashCode = HashCodeHelper.GetHashCode(Trackers, hashCode); + hashCode = HashCodeHelper.GetHashCode(IssueCustomFields, hashCode); + hashCode = HashCodeHelper.GetHashCode(CustomFieldValues, hashCode); + hashCode = HashCodeHelper.GetHashCode(IssueCategories, hashCode); + hashCode = HashCodeHelper.GetHashCode(EnabledModules, hashCode); + hashCode = HashCodeHelper.GetHashCode(TimeEntryActivities, hashCode); + hashCode = HashCodeHelper.GetHashCode(DefaultAssignee, hashCode); + hashCode = HashCodeHelper.GetHashCode(DefaultVersion, hashCode); + + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Project left, Project right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Project left, Project right) + { + return !Equals(left, right); + } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[Project: Id={Id.ToInvariantString()}, Name={Name}, Identifier={Identifier}, Status={Status:G}, IsPublic={IsPublic.ToInvariantString()}]"; + } +} diff --git a/src/redmine-net-api/Types/ProjectEnabledModule.cs b/src/redmine-net-api/Types/ProjectEnabledModule.cs new file mode 100644 index 00000000..eb91f016 --- /dev/null +++ b/src/redmine-net-api/Types/ProjectEnabledModule.cs @@ -0,0 +1,69 @@ +ο»Ώ/* +Copyright 2011 - 2025 Adrian Popescu + +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. +*/ + +using System; +using System.Diagnostics; +using System.Xml.Serialization; +using Redmine.Net.Api.Common; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// the module name: boards, calendar, documents, files, gant, issue_tracking, news, repository, time_tracking, wiki. + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.ENABLED_MODULE)] + public sealed class ProjectEnabledModule : IdentifiableName, IValue + { + #region Ctors + /// + /// + /// + public ProjectEnabledModule() { } + + /// + /// + /// + /// + public ProjectEnabledModule(string moduleName) + { + if (moduleName.IsNullOrWhiteSpace()) + { + throw new ArgumentException("The module name should be one of: boards, calendar, documents, files, gant, issue_tracking, news, repository, time_tracking, wiki.", nameof(moduleName)); + } + + Name = moduleName; + } + + #endregion + + #region Implementation of IValue + /// + /// + /// + public string Value => Name; + + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[ProjectEnabledModule: Id={Id.ToInvariantString()}, Name={Name}]"; + + } +} \ No newline at end of file diff --git a/redmine-net20-api/Types/TrackerCustomField.cs b/src/redmine-net-api/Types/ProjectIssueCategory.cs old mode 100755 new mode 100644 similarity index 63% rename from redmine-net20-api/Types/TrackerCustomField.cs rename to src/redmine-net-api/Types/ProjectIssueCategory.cs index 91305fd5..6f214692 --- a/redmine-net20-api/Types/TrackerCustomField.cs +++ b/src/redmine-net-api/Types/ProjectIssueCategory.cs @@ -1,5 +1,5 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. +ο»Ώ/* + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ You may obtain a copy of the License at limitations under the License. */ -using System.Xml; +using System.Diagnostics; using System.Xml.Serialization; using Redmine.Net.Api.Extensions; @@ -23,27 +23,25 @@ namespace Redmine.Net.Api.Types /// /// /// - [XmlRoot(RedmineKeys.TRACKER)] - public class TrackerCustomField : Tracker + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.ISSUE_CATEGORY)] + public sealed class ProjectIssueCategory : IdentifiableName { /// /// /// - /// - public override void ReadXml(XmlReader reader) + public ProjectIssueCategory() { } + + internal ProjectIssueCategory(int id, string name) + : base(id, name) { - Id = reader.ReadAttributeAsInt(RedmineKeys.ID); - Name = reader.GetAttribute(RedmineKeys.NAME); - reader.Read(); } /// /// /// /// - public override string ToString () - { - return string.Format ("[TrackerCustomField: {0}]", base.ToString()); - } + private string DebuggerDisplay => $"[ProjectIssueCategory: Id={Id.ToInvariantString()}, Name={Name}]"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/ProjectMembership.cs b/src/redmine-net-api/Types/ProjectMembership.cs new file mode 100644 index 00000000..64b6fb0d --- /dev/null +++ b/src/redmine-net-api/Types/ProjectMembership.cs @@ -0,0 +1,238 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System.Collections.Generic; +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// Availability 1.4 + /// + /// + /// POST - Adds a project member. + /// GET - Returns the membership of given :id. + /// PUT - Updates the membership of given :id. Only the roles can be updated, the project and the user of a membership are read-only. + /// DELETE - Deletes a memberships. Memberships inherited from a group membership can not be deleted. You must delete the group membership. + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.MEMBERSHIP)] + public sealed class ProjectMembership : Identifiable + { + #region Properties + /// + /// Gets or sets the project. + /// + /// The project. + public IdentifiableName Project { get; internal set; } + + /// + /// Gets or sets the user. + /// + /// + /// The user. + /// + public IdentifiableName User { get; set; } + + /// + /// Gets or sets the group. + /// + /// + /// The group. + /// + public IdentifiableName Group { get; internal set; } + + /// + /// Gets or sets the type. + /// + /// The type. + public List Roles { get; set; } + #endregion + + #region Implementation of IXmlSerialization + /// + /// + /// + /// + public override void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.GROUP: Group = new IdentifiableName(reader); break; + case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; + case RedmineKeys.ROLES: Roles = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.USER: User = new IdentifiableName(reader); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteXml(XmlWriter writer) + { + if (Id <= 0) + { + writer.WriteIdIfNotNull(RedmineKeys.USER_ID, User); + } + + writer.WriteArray(RedmineKeys.ROLE_IDS, Roles, root: RedmineKeys.ROLE_ID); + } + #endregion + + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.GROUP: Group = new IdentifiableName(reader); break; + case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; + case RedmineKeys.ROLES: Roles = reader.ReadAsCollection(); break; + case RedmineKeys.USER: User = new IdentifiableName(reader); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.MEMBERSHIP)) + { + if (Id <= 0) + { + writer.WriteIdIfNotNull(RedmineKeys.USER_ID, User); + } + + writer.WriteArray(RedmineKeys.ROLE_IDS, Roles); + } + } + #endregion + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public override bool Equals(ProjectMembership other) + { + if (other == null) return false; + return Id == other.Id + && Project == other.Project + && User == other.User + && Group == other.Group + && Roles != null ? Roles.Equals(other.Roles) : other.Roles == null; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as ProjectMembership); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Project, hashCode); + hashCode = HashCodeHelper.GetHashCode(User, hashCode); + hashCode = HashCodeHelper.GetHashCode(Group, hashCode); + hashCode = HashCodeHelper.GetHashCode(Roles, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(ProjectMembership left, ProjectMembership right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(ProjectMembership left, ProjectMembership right) + { + return !Equals(left, right); + } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[ProjectMembership: Id={Id.ToInvariantString()}]"; + + } +} \ No newline at end of file diff --git a/redmine-net20-api/Types/ProjectStatus.cs b/src/redmine-net-api/Types/ProjectStatus.cs similarity index 86% rename from redmine-net20-api/Types/ProjectStatus.cs rename to src/redmine-net-api/Types/ProjectStatus.cs index 302d98e3..1e98e1bb 100755 --- a/redmine-net20-api/Types/ProjectStatus.cs +++ b/src/redmine-net-api/Types/ProjectStatus.cs @@ -1,5 +1,5 @@ ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,6 +21,10 @@ namespace Redmine.Net.Api.Types /// public enum ProjectStatus { + /// + /// value of zero - Not set/unknown + /// + None, /// /// /// diff --git a/redmine-net20-api/Types/ProjectIssueCategory.cs b/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs old mode 100755 new mode 100644 similarity index 56% rename from redmine-net20-api/Types/ProjectIssueCategory.cs rename to src/redmine-net-api/Types/ProjectTimeEntryActivity.cs index 28003cb6..823de240 --- a/redmine-net20-api/Types/ProjectIssueCategory.cs +++ b/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs @@ -1,5 +1,5 @@ ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,23 +14,34 @@ You may obtain a copy of the License at limitations under the License. */ +using System.Diagnostics; using System.Xml.Serialization; +using Redmine.Net.Api.Extensions; namespace Redmine.Net.Api.Types { /// /// /// - [XmlRoot(RedmineKeys.ISSUE_CATEGORY)] - public class ProjectIssueCategory : IdentifiableName + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.TIME_ENTRY_ACTIVITY)] + public sealed class ProjectTimeEntryActivity : IdentifiableName { + /// + /// + /// + public ProjectTimeEntryActivity() { } + + internal ProjectTimeEntryActivity(int id, string name) + : base(id, name) + { + } + /// /// /// /// - public override string ToString () - { - return string.Format ("[ProjectIssueCategory: {0}]", base.ToString()); - } + private string DebuggerDisplay => $"[ProjectTimeEntryActivity: Id={Id.ToInvariantString()}, Name={Name}]"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/ProjectTracker.cs b/src/redmine-net-api/Types/ProjectTracker.cs new file mode 100644 index 00000000..a3c93011 --- /dev/null +++ b/src/redmine-net-api/Types/ProjectTracker.cs @@ -0,0 +1,71 @@ +ο»Ώ/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System.Diagnostics; +using System.Xml.Serialization; +using Redmine.Net.Api.Common; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.TRACKER)] + public sealed class ProjectTracker : IdentifiableName, IValue + { + /// + /// + /// + public ProjectTracker() { } + + /// + /// + /// + /// the tracker id: 1 for Bug, etc. + /// + public ProjectTracker(int trackerId, string name) + : base(trackerId, name) + { + } + + /// + /// + /// + /// + internal ProjectTracker(int trackerId) + { + Id = trackerId; + } + + #region Implementation of IValue + + /// + /// + /// + public string Value => Id.ToInvariantString(); + + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[ProjectTracker: Id={Id.ToInvariantString()}, Name={Name}]"; + + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/Query.cs b/src/redmine-net-api/Types/Query.cs new file mode 100644 index 00000000..97ade4da --- /dev/null +++ b/src/redmine-net-api/Types/Query.cs @@ -0,0 +1,179 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// Availability 1.3 + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.QUERY)] + public sealed class Query : IdentifiableName, IEquatable + { + #region Properties + /// + /// Gets a value indicating whether this instance is public. + /// + /// true if this instance is public; otherwise, false. + public bool IsPublic { get; internal set; } + + /// + /// Gets the project id. + /// + /// The project id. + public int? ProjectId { get; internal set; } + #endregion + + #region Implementation of IXmlSerialization + /// + /// + /// + /// + public override void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.IS_PUBLIC: IsPublic = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; + case RedmineKeys.PROJECT_ID: ProjectId = reader.ReadElementContentAsNullableInt(); break; + default: reader.Read(); break; + } + } + } + #endregion + + #region Implementation of IJsonSerialization + + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.IS_PUBLIC: IsPublic = reader.ReadAsBool(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.PROJECT_ID: ProjectId = reader.ReadAsInt32(); break; + default: reader.Read(); break; + } + } + } + #endregion + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public bool Equals(Query other) + { + if (other == null) return false; + + return base.Equals(other) + && IsPublic == other.IsPublic + && ProjectId == other.ProjectId; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as Query); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(IsPublic, hashCode); + hashCode = HashCodeHelper.GetHashCode(ProjectId, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Query left, Query right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Query left, Query right) + { + return !Equals(left, right); + } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[Query: Id={Id.ToInvariantString()}, Name={Name}, IsPublic={IsPublic.ToInvariantString()}, ProjectId={ProjectId?.ToInvariantString()}]"; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/Role.cs b/src/redmine-net-api/Types/Role.cs new file mode 100644 index 00000000..33052455 --- /dev/null +++ b/src/redmine-net-api/Types/Role.cs @@ -0,0 +1,210 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// Availability 1.4 + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.ROLE)] + public sealed class Role : IdentifiableName, IEquatable + { + #region Properties + /// + /// Gets the permissions. + /// + /// + /// The issue relations. + /// + public List Permissions { get; internal set; } + + /// + /// + /// + public string IssuesVisibility { get; set; } + + /// + /// + /// + public string TimeEntriesVisibility { get; set; } + + /// + /// + /// + public string UsersVisibility { get; set; } + + /// + /// + /// + public bool? IsAssignable { get; set; } + #endregion + + #region Implementation of IXmlSerialization + /// + /// + /// + /// + public override void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; + case RedmineKeys.ASSIGNABLE: IsAssignable = reader.ReadElementContentAsNullableBoolean(); break; + case RedmineKeys.ISSUES_VISIBILITY: IssuesVisibility = reader.ReadElementContentAsString(); break; + case RedmineKeys.TIME_ENTRIES_VISIBILITY: TimeEntriesVisibility = reader.ReadElementContentAsString(); break; + case RedmineKeys.USERS_VISIBILITY: UsersVisibility = reader.ReadElementContentAsString(); break; + case RedmineKeys.PERMISSIONS: Permissions = reader.ReadElementContentAsCollection(); break; + default: reader.Read(); break; + } + } + } + #endregion + + #region Implementation of IJsonSerialization + + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.ASSIGNABLE: IsAssignable = reader.ReadAsBoolean(); break; + case RedmineKeys.ISSUES_VISIBILITY: IssuesVisibility = reader.ReadAsString(); break; + case RedmineKeys.TIME_ENTRIES_VISIBILITY: TimeEntriesVisibility = reader.ReadAsString(); break; + case RedmineKeys.USERS_VISIBILITY: UsersVisibility = reader.ReadAsString(); break; + case RedmineKeys.PERMISSIONS: Permissions = reader.ReadAsCollection(); break; + default: reader.Read(); break; + } + } + } + #endregion + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public bool Equals(Role other) + { + if (other == null) return false; + return Id == other.Id + && string.Equals(Name, other.Name, StringComparison.Ordinal) + && IsAssignable == other.IsAssignable + && IssuesVisibility == other.IssuesVisibility + && TimeEntriesVisibility == other.TimeEntriesVisibility + && UsersVisibility == other.UsersVisibility + && Permissions != null ? Permissions.Equals(other.Permissions) : other.Permissions == null; + + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as Role); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(IsAssignable, hashCode); + hashCode = HashCodeHelper.GetHashCode(IssuesVisibility, hashCode); + hashCode = HashCodeHelper.GetHashCode(TimeEntriesVisibility, hashCode); + hashCode = HashCodeHelper.GetHashCode(UsersVisibility, hashCode); + hashCode = HashCodeHelper.GetHashCode(Permissions, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Role left, Role right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Role left, Role right) + { + return !Equals(left, right); + } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[Role: Id={Id.ToInvariantString()}, Name={Name}]"; + + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/Search.cs b/src/redmine-net-api/Types/Search.cs new file mode 100644 index 00000000..e24cd5cf --- /dev/null +++ b/src/redmine-net-api/Types/Search.cs @@ -0,0 +1,185 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Diagnostics; +using System.Xml; +using System.Xml.Schema; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.RESULT)] + public sealed class Search: IXmlSerializable, IJsonSerializable, IEquatable + { + /// + /// + /// + public int Id { get; set; } + /// + /// + /// + public string Title { get; set; } + /// + /// + /// + public string Type { get; set; } + /// + /// + /// + public string Url { get; set; } + /// + /// + /// + public string Description { get; set; } + /// + /// + /// + public DateTime? DateTime { get; set; } + + /// + public XmlSchema GetSchema() { return null; } + + /// + public void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; + case RedmineKeys.DATE_TIME: DateTime = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.URL: Url = reader.ReadElementContentAsString(); break; + case RedmineKeys.TYPE: Type = reader.ReadElementContentAsString(); break; + case RedmineKeys.TITLE: Title = reader.ReadElementContentAsString(); break; + default: reader.Read(); break; + } + } + } + + /// + public void WriteXml(XmlWriter writer) + { + } + + /// + public void WriteJson(JsonWriter writer) + { + } + + /// + public void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadAsString(); break; + case RedmineKeys.DATE_TIME: DateTime = reader.ReadAsDateTime(); break; + case RedmineKeys.URL: Url = reader.ReadAsString(); break; + case RedmineKeys.TYPE: Type = reader.ReadAsString(); break; + case RedmineKeys.TITLE: Title = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + + /// + public bool Equals(Search other) + { + if (other == null) return false; + return Id == other.Id + && string.Equals(Title, other.Title, StringComparison.Ordinal) + && string.Equals(Description, other.Description, StringComparison.Ordinal) + && string.Equals(Url, other.Url, StringComparison.Ordinal) + && string.Equals(Type, other.Type, StringComparison.Ordinal) + && DateTime == other.DateTime; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as Search); + } + + /// + public override int GetHashCode() + { + var hashCode = 397; + hashCode = HashCodeHelper.GetHashCode(Id, hashCode); + hashCode = HashCodeHelper.GetHashCode(Title, hashCode); + hashCode = HashCodeHelper.GetHashCode(Type, hashCode); + hashCode = HashCodeHelper.GetHashCode(Url, hashCode); + hashCode = HashCodeHelper.GetHashCode(Description, hashCode); + hashCode = HashCodeHelper.GetHashCode(DateTime, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Search left, Search right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Search left, Search right) + { + return !Equals(left, right); + } + + private string DebuggerDisplay => $"[Search: Id={Id.ToInvariantString()}, Title={Title}, Type={Type}]"; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/TimeEntry.cs b/src/redmine-net-api/Types/TimeEntry.cs new file mode 100644 index 00000000..ac98d3d7 --- /dev/null +++ b/src/redmine-net-api/Types/TimeEntry.cs @@ -0,0 +1,330 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + + +namespace Redmine.Net.Api.Types +{ + /// + /// Availability 1.1 + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.TIME_ENTRY)] + public sealed class TimeEntry : Identifiable + , ICloneable + { + #region Properties + private string comments; + + /// + /// Gets or sets the issue id to log time on. + /// + /// The issue id. + public IdentifiableName Issue { get; set; } + + /// + /// Gets or sets the project id to log time on. + /// + /// The project id. + public IdentifiableName Project { get; set; } + + /// + /// Gets or sets the date the time was spent (default to the current date). + /// + /// The spent on. + public DateTime? SpentOn { get; set; } + + /// + /// Gets or sets the number of spent hours. + /// + /// The hours. + public decimal Hours { get; set; } + + /// + /// Gets or sets the activity id of the time activity. This parameter is required unless a default activity is defined in Redmine. + /// + /// The activity id. + public IdentifiableName Activity { get; set; } + + /// + /// Gets the user. + /// + /// + /// The user. + /// + public IdentifiableName User { get; internal set; } + + /// + /// Gets or sets the short description for the entry (255 characters max). + /// + /// The comments. + public string Comments + { + get => comments; + set => comments = value.Truncate(255); + } + + /// + /// Gets the created on. + /// + /// The created on. + public DateTime? CreatedOn { get; internal set; } + + /// + /// Gets the updated on. + /// + /// The updated on. + public DateTime? UpdatedOn { get; internal set; } + + /// + /// Gets or sets the custom fields. + /// + /// The custom fields. + public List CustomFields { get; set; } + #endregion + + #region Implementation of IXmlSerialization + /// + /// + /// + /// + public override void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.ACTIVITY: Activity = new IdentifiableName(reader); break; + case RedmineKeys.ACTIVITY_ID: Activity = new IdentifiableName(reader); break; + case RedmineKeys.COMMENTS: Comments = reader.ReadElementContentAsString(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.HOURS: Hours = reader.ReadElementContentAsDecimal(); break; + case RedmineKeys.ISSUE_ID: Issue = new IdentifiableName(reader); break; + case RedmineKeys.ISSUE: Issue = new IdentifiableName(reader); break; + case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; + case RedmineKeys.PROJECT_ID: Project = new IdentifiableName(reader); break; + case RedmineKeys.SPENT_ON: SpentOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.USER: User = new IdentifiableName(reader); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteXml(XmlWriter writer) + { + writer.WriteIdIfNotNull(RedmineKeys.ISSUE_ID, Issue); + writer.WriteIdIfNotNull(RedmineKeys.PROJECT_ID, Project); + writer.WriteDateOrEmpty(RedmineKeys.SPENT_ON, SpentOn.GetValueOrDefault(DateTime.Now)); + writer.WriteValueOrEmpty(RedmineKeys.HOURS, Hours); + writer.WriteIdIfNotNull(RedmineKeys.ACTIVITY_ID, Activity); + writer.WriteElementString(RedmineKeys.COMMENTS, Comments); + writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); + } + #endregion + + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.ACTIVITY: Activity = new IdentifiableName(reader); break; + case RedmineKeys.ACTIVITY_ID: Activity = new IdentifiableName(reader); break; + case RedmineKeys.COMMENTS: Comments = reader.ReadAsString(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadAsCollection(); break; + case RedmineKeys.HOURS: Hours = reader.ReadAsDecimal().GetValueOrDefault(); break; + case RedmineKeys.ISSUE: Issue = new IdentifiableName(reader); break; + case RedmineKeys.ISSUE_ID: Issue = new IdentifiableName(reader); break; + case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; + case RedmineKeys.PROJECT_ID: Project = new IdentifiableName(reader); break; + case RedmineKeys.SPENT_ON: SpentOn = reader.ReadAsDateTime(); break; + case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.USER: User = new IdentifiableName(reader); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.TIME_ENTRY)) + { + writer.WriteIdIfNotNull(RedmineKeys.ISSUE_ID, Issue); + writer.WriteIdIfNotNull(RedmineKeys.PROJECT_ID, Project); + writer.WriteIdIfNotNull(RedmineKeys.ACTIVITY_ID, Activity); + writer.WriteDateOrEmpty(RedmineKeys.SPENT_ON, SpentOn.GetValueOrDefault(DateTime.Now)); + writer.WriteProperty(RedmineKeys.HOURS, Hours); + writer.WriteProperty(RedmineKeys.COMMENTS, Comments); + writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); + } + } + #endregion + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public override bool Equals(TimeEntry other) + { + if (other == null) return false; + return Id == other.Id + && Issue == other.Issue + && Project == other.Project + && SpentOn == other.SpentOn + && Hours == other.Hours + && Activity == other.Activity + && string.Equals(Comments, other.Comments, StringComparison.Ordinal) + && User == other.User + && CreatedOn == other.CreatedOn + && UpdatedOn == other.UpdatedOn + && (CustomFields?.Equals(other.CustomFields) ?? other.CustomFields == null); + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as TimeEntry); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Issue, hashCode); + hashCode = HashCodeHelper.GetHashCode(Project, hashCode); + hashCode = HashCodeHelper.GetHashCode(SpentOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(Hours, hashCode); + hashCode = HashCodeHelper.GetHashCode(Activity, hashCode); + hashCode = HashCodeHelper.GetHashCode(User, hashCode); + hashCode = HashCodeHelper.GetHashCode(Comments, hashCode); + hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(TimeEntry left, TimeEntry right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(TimeEntry left, TimeEntry right) + { + return !Equals(left, right); + } + #endregion + + #region Implementation of ICloneable + /// + /// + /// + /// + public new TimeEntry Clone(bool resetId) + { + var timeEntry = new TimeEntry + { + Activity = Activity, + Comments = Comments, + Hours = Hours, + Issue = Issue, + Project = Project, + SpentOn = SpentOn, + User = User, + CustomFields = CustomFields + }; + return timeEntry; + } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[TimeEntry: Id={Id.ToInvariantString()}, SpentOn={SpentOn?.ToString("u", CultureInfo.InvariantCulture)}, Hours={Hours.ToString("F", CultureInfo.InvariantCulture)}]"; + + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/TimeEntryActivity.cs b/src/redmine-net-api/Types/TimeEntryActivity.cs new file mode 100644 index 00000000..0e9a2d46 --- /dev/null +++ b/src/redmine-net-api/Types/TimeEntryActivity.cs @@ -0,0 +1,195 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// Availability 2.2 + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.TIME_ENTRY_ACTIVITY)] + public sealed class TimeEntryActivity : IdentifiableName, IEquatable + { + /// + /// + /// + public TimeEntryActivity() { } + + internal TimeEntryActivity(int id, string name) + : base(id, name) + { + } + + #region Properties + /// + /// + /// + public bool IsDefault { get; internal set; } + + /// + /// + /// + public bool IsActive { get; internal set; } + #endregion + + #region Implementation of IXmlSerializable + + /// + /// Generates an object from its XML representation. + /// + /// The stream from which the object is deserialized. + public override void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.IS_DEFAULT: IsDefault = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; + case RedmineKeys.ACTIVE: IsActive = reader.ReadElementContentAsBoolean(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteXml(XmlWriter writer) { } + + #endregion + + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.IS_DEFAULT: IsDefault = reader.ReadAsBool(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.ACTIVE: IsActive = reader.ReadAsBool(); break; + default: reader.Read(); break; + } + } + } + #endregion + + #region Implementation of IEquatable + + /// + /// + /// + /// + /// + public bool Equals(TimeEntryActivity other) + { + if (other == null) return false; + + return base.Equals(other) + && IsDefault == other.IsDefault + && IsActive == other.IsActive; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as TimeEntryActivity); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(IsDefault, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsActive, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(TimeEntryActivity left, TimeEntryActivity right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(TimeEntryActivity left, TimeEntryActivity right) + { + return !Equals(left, right); + } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[TimeEntryActivity: Id={Id.ToInvariantString()}, Name={Name}, IsDefault={IsDefault.ToInvariantString()}, IsActive={IsActive.ToInvariantString()}]"; + + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/Tracker.cs b/src/redmine-net-api/Types/Tracker.cs new file mode 100644 index 00000000..ff9576a6 --- /dev/null +++ b/src/redmine-net-api/Types/Tracker.cs @@ -0,0 +1,187 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// Availability 1.3 + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.TRACKER)] + public class Tracker : IdentifiableName, IEquatable + { + /// + /// Gets the default (issue) status for this tracker. + /// + public IdentifiableName DefaultStatus { get; internal set; } + + /// + /// Gets the description of this tracker. + /// + public string Description { get; internal set; } + + /// + /// Gets the list of enabled tracker's core fields + /// + public List EnabledStandardFields { get; internal set; } + + #region Implementation of IXmlSerialization + /// + /// Generates an object from its XML representation. + /// + /// The stream from which the object is deserialized. + public override void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; + case RedmineKeys.DEFAULT_STATUS: DefaultStatus = new IdentifiableName(reader); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; + case RedmineKeys.ENABLED_STANDARD_FIELDS: EnabledStandardFields = reader.ReadElementContentAsCollection(); break; + default: reader.Read(); break; + } + } + } + #endregion + + #region Implementation of IJsonSerialization + + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.DEFAULT_STATUS: DefaultStatus = new IdentifiableName(reader); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadAsString(); break; + case RedmineKeys.ENABLED_STANDARD_FIELDS: EnabledStandardFields = reader.ReadAsCollection(); break; + default: reader.Read(); break; + } + } + } + #endregion + + #region Implementation of IEquatable + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// An object to compare with this object. + /// + /// true if the current object is equal to the parameter; otherwise, false. + /// + public bool Equals(Tracker other) + { + if (other == null) return false; + + return base.Equals(other) + && DefaultStatus == other.DefaultStatus + && string.Equals(Description, other.Description, StringComparison.Ordinal) + && EnabledStandardFields != null ? EnabledStandardFields.Equals(other.EnabledStandardFields) : other.EnabledStandardFields != null; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as Tracker); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + int hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(DefaultStatus, hashCode); + hashCode = HashCodeHelper.GetHashCode(Description, hashCode); + hashCode = HashCodeHelper.GetHashCode(EnabledStandardFields, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Tracker left, Tracker right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Tracker left, Tracker right) + { + return !Equals(left, right); + } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[Tracker: Id={Id.ToInvariantString()}, Name={Name}]"; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/TrackerCoreField.cs b/src/redmine-net-api/Types/TrackerCoreField.cs new file mode 100644 index 00000000..f46b1cb5 --- /dev/null +++ b/src/redmine-net-api/Types/TrackerCoreField.cs @@ -0,0 +1,157 @@ +using System; +using System.Diagnostics; +using System.Xml; +using System.Xml.Schema; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.FIELD)] + public sealed class TrackerCoreField: IXmlSerializable, IJsonSerializable, IEquatable + { + /// + /// + /// + public TrackerCoreField() + { + } + + internal TrackerCoreField(string name) + { + Name = name; + } + /// + /// + /// + public string Name { get; private set; } + + /// + /// + /// + /// + private string DebuggerDisplay => $"[TrackerCoreField: Name={Name}]"; + + /// + /// + /// + /// + public XmlSchema GetSchema() + { + return null; + } + + /// + /// + /// + /// + public void ReadXml(XmlReader reader) + { + reader.Read(); + if (reader.NodeType == XmlNodeType.Text) + { + Name = reader.Value; + } + } + + /// + /// + /// + /// + public void WriteXml(XmlWriter writer) { } + + /// + /// + /// + /// + public void WriteJson(JsonWriter writer) { } + + /// + /// + /// + /// + public void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.PERMISSION: Name = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + /// + public bool Equals(TrackerCoreField other) + { + return other != null && string.Equals(Name, other.Name, StringComparison.Ordinal); + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as TrackerCoreField); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(TrackerCoreField left, TrackerCoreField right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(TrackerCoreField left, TrackerCoreField right) + { + return !Equals(left, right); + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/TrackerCustomField.cs b/src/redmine-net-api/Types/TrackerCustomField.cs new file mode 100644 index 00000000..6dd1d213 --- /dev/null +++ b/src/redmine-net-api/Types/TrackerCustomField.cs @@ -0,0 +1,84 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.TRACKER)] + public sealed class TrackerCustomField : Tracker + { + #region Implementation of IXmlSerialization + /// + /// + /// + /// + public override void ReadXml(XmlReader reader) + { + Id = reader.ReadAttributeAsInt(RedmineKeys.ID); + Name = reader.GetAttribute(RedmineKeys.NAME); + reader.Read(); + } + #endregion + + #region Implementation of IJsonSerialization + + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[TrackerCustomField: Id={Id.ToInvariantString()}, Name={Name}]"; + + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/Upload.cs b/src/redmine-net-api/Types/Upload.cs new file mode 100644 index 00000000..657572dc --- /dev/null +++ b/src/redmine-net-api/Types/Upload.cs @@ -0,0 +1,250 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Diagnostics; +using System.Xml; +using System.Xml.Schema; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// Support for adding attachments through the REST API is added in Redmine 1.4.0. + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.UPLOAD)] + public sealed class Upload : IXmlSerializable, IJsonSerializable, IEquatable + , ICloneable + { + #region Properties + /// + /// Gets the uploaded id. + /// + public string Id { get; private set; } + + /// + /// Gets or sets the uploaded token. + /// + /// The name of the file. + public string Token { get; set; } + + /// + /// Gets or sets the name of the file. + /// Maximum allowed file size (1024000). + /// + /// The name of the file. + public string FileName { get; set; } + + /// + /// Gets or sets the name of the file. + /// + /// The name of the file. + public string ContentType { get; set; } + + /// + /// Gets or sets the file description. (Undocumented feature) + /// + /// The file descroΓΌtopm. + public string Description { get; set; } + #endregion + + #region Implementation of IXmlSerialization + /// + /// + /// + /// + public XmlSchema GetSchema() { return null; } + + /// + /// + /// + /// + public void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsString(); break; + case RedmineKeys.CONTENT_TYPE: ContentType = reader.ReadElementContentAsString(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; + case RedmineKeys.FILE_NAME: FileName = reader.ReadElementContentAsString(); break; + case RedmineKeys.TOKEN: Token = reader.ReadElementContentAsString(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public void WriteXml(XmlWriter writer) + { + writer.WriteElementString(RedmineKeys.TOKEN, Token); + writer.WriteElementString(RedmineKeys.CONTENT_TYPE, ContentType); + writer.WriteElementString(RedmineKeys.FILE_NAME, FileName); + writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); + } + #endregion + + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsString(); break; + case RedmineKeys.CONTENT_TYPE: ContentType = reader.ReadAsString(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadAsString(); break; + case RedmineKeys.FILE_NAME: FileName = reader.ReadAsString(); break; + case RedmineKeys.TOKEN: Token = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public void WriteJson(JsonWriter writer) + { + writer.WriteStartObject(); + writer.WriteProperty(RedmineKeys.TOKEN, Token); + writer.WriteProperty(RedmineKeys.CONTENT_TYPE, ContentType); + writer.WriteProperty(RedmineKeys.FILE_NAME, FileName); + writer.WriteProperty(RedmineKeys.DESCRIPTION, Description); + writer.WriteEndObject(); + } + #endregion + + #region Implementation of IEquatable + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// An object to compare with this object. + /// + /// true if the current object is equal to the parameter; otherwise, false. + /// + public bool Equals(Upload other) + { + return other != null + && string.Equals(Token, other.Token, StringComparison.Ordinal) + && string.Equals(FileName, other.FileName, StringComparison.Ordinal) + && string.Equals(Description, other.Description, StringComparison.Ordinal) + && string.Equals(ContentType, other.ContentType, StringComparison.Ordinal); + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as Upload); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Token, hashCode); + hashCode = HashCodeHelper.GetHashCode(FileName, hashCode); + hashCode = HashCodeHelper.GetHashCode(Description, hashCode); + hashCode = HashCodeHelper.GetHashCode(ContentType, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Upload left, Upload right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Upload left, Upload right) + { + return !Equals(left, right); + } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[Upload: Token={Token}, FileName={FileName}]"; + + /// + /// + /// + /// + public Upload Clone(bool resetId) + { + return new Upload + { + Token = Token, + FileName = FileName, + ContentType = ContentType, + Description = Description + }; + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs new file mode 100644 index 00000000..06b0b46c --- /dev/null +++ b/src/redmine-net-api/Types/User.cs @@ -0,0 +1,444 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// Availability 1.1 + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.USER)] + public sealed class User : Identifiable + { + #region Properties + /// + /// Gets or sets the user avatar url. + /// + public string AvatarUrl { get; set; } + + /// + /// Gets or sets the user login. + /// + /// The login. + public string Login { get; set; } + + /// + /// Gets or sets the user password. + /// + /// The password. + public string Password { get; set; } + + /// + /// Gets or sets the first name. + /// + /// The first name. + public string FirstName { get; set; } + + /// + /// Gets or sets the last name. + /// + /// The last name. + public string LastName { get; set; } + + /// + /// Gets or sets the email. + /// + /// The email. + public string Email { get; set; } + + /// + /// + /// + public bool IsAdmin { get; set; } + + /// + /// twofa_scheme + /// + public string TwoFactorAuthenticationScheme { get; set; } + + /// + /// Gets or sets the authentication mode id. + /// + /// + /// The authentication mode id. + /// + public int? AuthenticationModeId { get; set; } + + /// + /// Gets the created on. + /// + /// The created on. + public DateTime? CreatedOn { get; internal set; } + + /// + /// Gets the last login on. + /// + /// The last login on. + public DateTime? LastLoginOn { get; internal set; } + + /// + /// Gets the API key of the user, visible for admins and for yourself (added in 2.3.0) + /// + public string ApiKey { get; internal set; } + + /// + /// Gets the status of the user, visible for admins only (added in 2.4.0) + /// + public UserStatus Status { get; set; } + + /// + /// + /// + public bool MustChangePassword { get; set; } + + /// + /// + /// + public bool GeneratePassword { get; set; } + + /// + /// + /// + public DateTime? PasswordChangedOn { get; set; } + + /// + /// + /// + public DateTime? UpdatedOn { get; set; } + + /// + /// Gets or sets the custom fields. + /// + /// The custom fields. + public List CustomFields { get; set; } + + /// + /// Gets or sets the memberships. + /// + /// + /// The memberships. + /// + public List Memberships { get; internal set; } + + /// + /// Gets or sets the user's groups. + /// + /// + /// The groups. + /// + public List Groups { get; internal set; } + + /// + /// Gets or sets the user's mail_notification. + /// + /// + /// only_my_events, only_assigned, only_owner + /// + public string MailNotification { get; set; } + + /// + /// Send account information to the user + /// + public bool SendInformation { get; set; } + + #endregion + + #region Implementation of IXmlSerialization + /// + /// + /// + /// + public override void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.ADMIN: IsAdmin = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.API_KEY: ApiKey = reader.ReadElementContentAsString(); break; + case RedmineKeys.AUTH_SOURCE_ID: AuthenticationModeId = reader.ReadElementContentAsNullableInt(); break; + case RedmineKeys.AVATAR_URL: AvatarUrl = reader.ReadElementContentAsString(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.FIRST_NAME: FirstName = reader.ReadElementContentAsString(); break; + case RedmineKeys.GROUPS: Groups = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.LAST_LOGIN_ON: LastLoginOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.LAST_NAME: LastName = reader.ReadElementContentAsString(); break; + case RedmineKeys.LOGIN: Login = reader.ReadElementContentAsString(); break; + case RedmineKeys.MAIL: Email = reader.ReadElementContentAsString(); break; + case RedmineKeys.MAIL_NOTIFICATION: MailNotification = reader.ReadElementContentAsString(); break; + case RedmineKeys.MEMBERSHIPS: Memberships = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.MUST_CHANGE_PASSWORD: MustChangePassword = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.PASSWORD_CHANGED_ON: PasswordChangedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.STATUS: Status = (UserStatus)reader.ReadElementContentAsInt(); break; + case RedmineKeys.TWO_FA_SCHEME: TwoFactorAuthenticationScheme = reader.ReadElementContentAsString(); break; + case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteXml(XmlWriter writer) + { + writer.WriteElementString(RedmineKeys.LOGIN, Login); + + if (!Password.IsNullOrWhiteSpace()) + { + writer.WriteElementString(RedmineKeys.PASSWORD, Password); + } + + writer.WriteElementString(RedmineKeys.FIRST_NAME, FirstName); + writer.WriteElementString(RedmineKeys.LAST_NAME, LastName); + writer.WriteElementString(RedmineKeys.MAIL, Email); + + if(AuthenticationModeId.HasValue) + { + writer.WriteValueOrEmpty(RedmineKeys.AUTH_SOURCE_ID, AuthenticationModeId); + } + + if(!MailNotification.IsNullOrWhiteSpace()) + { + writer.WriteElementString(RedmineKeys.MAIL_NOTIFICATION, MailNotification); + } + + writer.WriteBoolean(RedmineKeys.MUST_CHANGE_PASSWORD, MustChangePassword); + writer.WriteBoolean(RedmineKeys.GENERATE_PASSWORD, GeneratePassword); + writer.WriteBoolean(RedmineKeys.SEND_INFORMATION, SendInformation); + + writer.WriteElementString(RedmineKeys.STATUS, ((int)Status).ToInvariantString()); + + writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); + } + #endregion + + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.ADMIN: IsAdmin = reader.ReadAsBool(); break; + case RedmineKeys.API_KEY: ApiKey = reader.ReadAsString(); break; + case RedmineKeys.AUTH_SOURCE_ID: AuthenticationModeId = reader.ReadAsInt32(); break; + case RedmineKeys.AVATAR_URL: AvatarUrl = reader.ReadAsString(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadAsCollection(); break; + case RedmineKeys.LAST_LOGIN_ON: LastLoginOn = reader.ReadAsDateTime(); break; + case RedmineKeys.LAST_NAME: LastName = reader.ReadAsString(); break; + case RedmineKeys.LOGIN: Login = reader.ReadAsString(); break; + case RedmineKeys.FIRST_NAME: FirstName = reader.ReadAsString(); break; + case RedmineKeys.GROUPS: Groups = reader.ReadAsCollection(); break; + case RedmineKeys.MAIL: Email = reader.ReadAsString(); break; + case RedmineKeys.MAIL_NOTIFICATION: MailNotification = reader.ReadAsString(); break; + case RedmineKeys.MEMBERSHIPS: Memberships = reader.ReadAsCollection(); break; + case RedmineKeys.MUST_CHANGE_PASSWORD: MustChangePassword = reader.ReadAsBool(); break; + case RedmineKeys.PASSWORD_CHANGED_ON: PasswordChangedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.STATUS: Status = (UserStatus)reader.ReadAsInt(); break; + case RedmineKeys.TWO_FA_SCHEME: TwoFactorAuthenticationScheme = reader.ReadAsString(); break; + case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadAsDateTime(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.USER)) + { + writer.WriteProperty(RedmineKeys.LOGIN, Login); + + if (!string.IsNullOrEmpty(Password)) + { + writer.WriteProperty(RedmineKeys.PASSWORD, Password); + } + + writer.WriteProperty(RedmineKeys.FIRST_NAME, FirstName); + writer.WriteProperty(RedmineKeys.LAST_NAME, LastName); + writer.WriteProperty(RedmineKeys.MAIL, Email); + + if(AuthenticationModeId.HasValue) + { + writer.WriteValueOrEmpty(RedmineKeys.AUTH_SOURCE_ID, AuthenticationModeId); + } + + if(!MailNotification.IsNullOrWhiteSpace()) + { + writer.WriteProperty(RedmineKeys.MAIL_NOTIFICATION, MailNotification); + } + + writer.WriteBoolean(RedmineKeys.MUST_CHANGE_PASSWORD, MustChangePassword); + writer.WriteBoolean(RedmineKeys.GENERATE_PASSWORD, GeneratePassword); + writer.WriteBoolean(RedmineKeys.SEND_INFORMATION, SendInformation); + + writer.WriteProperty(RedmineKeys.STATUS, ((int)Status).ToInvariantString()); + + writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); + } + } + #endregion + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public override bool Equals(User other) + { + if (other == null) return false; + return Id == other.Id + && string.Equals(AvatarUrl,other.AvatarUrl, StringComparison.Ordinal) + && string.Equals(Login,other.Login, StringComparison.Ordinal) + && string.Equals(FirstName,other.FirstName, StringComparison.Ordinal) + && string.Equals(LastName,other.LastName, StringComparison.Ordinal) + && string.Equals(Email,other.Email, StringComparison.Ordinal) + && string.Equals(MailNotification,other.MailNotification, StringComparison.Ordinal) + && string.Equals(ApiKey,other.ApiKey, StringComparison.Ordinal) + && string.Equals(TwoFactorAuthenticationScheme,other.TwoFactorAuthenticationScheme, StringComparison.Ordinal) + && AuthenticationModeId == other.AuthenticationModeId + && CreatedOn == other.CreatedOn + && LastLoginOn == other.LastLoginOn + && Status == other.Status + && MustChangePassword == other.MustChangePassword + && GeneratePassword == other.GeneratePassword + && SendInformation == other.SendInformation + && IsAdmin == other.IsAdmin + && PasswordChangedOn == other.PasswordChangedOn + && UpdatedOn == other.UpdatedOn + && CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null + && Memberships != null ? Memberships.Equals(other.Memberships) : other.Memberships == null + && Groups != null ? Groups.Equals(other.Groups) : other.Groups == null; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as User); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Id, hashCode); + hashCode = HashCodeHelper.GetHashCode(AvatarUrl, hashCode); + hashCode = HashCodeHelper.GetHashCode(Login, hashCode); + hashCode = HashCodeHelper.GetHashCode(FirstName, hashCode); + hashCode = HashCodeHelper.GetHashCode(LastName, hashCode); + hashCode = HashCodeHelper.GetHashCode(Email, hashCode); + hashCode = HashCodeHelper.GetHashCode(MailNotification, hashCode); + hashCode = HashCodeHelper.GetHashCode(ApiKey, hashCode); + hashCode = HashCodeHelper.GetHashCode(TwoFactorAuthenticationScheme, hashCode); + hashCode = HashCodeHelper.GetHashCode(AuthenticationModeId, hashCode); + hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(LastLoginOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(Status, hashCode); + hashCode = HashCodeHelper.GetHashCode(MustChangePassword, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsAdmin, hashCode); + hashCode = HashCodeHelper.GetHashCode(PasswordChangedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); + hashCode = HashCodeHelper.GetHashCode(Memberships, hashCode); + hashCode = HashCodeHelper.GetHashCode(Groups, hashCode); + hashCode = HashCodeHelper.GetHashCode(GeneratePassword, hashCode); + hashCode = HashCodeHelper.GetHashCode(SendInformation, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(User left, User right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(User left, User right) + { + return !Equals(left, right); + } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[User: Id={Id.ToInvariantString()}, Login={Login}, IsAdmin={IsAdmin.ToInvariantString()}, Status={Status:G}]"; + } +} \ No newline at end of file diff --git a/redmine-net20-api/Types/UserGroup.cs b/src/redmine-net-api/Types/UserGroup.cs old mode 100755 new mode 100644 similarity index 72% rename from redmine-net20-api/Types/UserGroup.cs rename to src/redmine-net-api/Types/UserGroup.cs index c7749959..55697d26 --- a/redmine-net20-api/Types/UserGroup.cs +++ b/src/redmine-net-api/Types/UserGroup.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2017 Adrian Popescu. + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,23 +14,24 @@ You may obtain a copy of the License at limitations under the License. */ +using System.Diagnostics; using System.Xml.Serialization; +using Redmine.Net.Api.Extensions; namespace Redmine.Net.Api.Types { /// /// /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.GROUP)] - public class UserGroup : IdentifiableName + public sealed class UserGroup : IdentifiableName { /// /// /// /// - public override string ToString () - { - return string.Format ("[UserGroup: {0}]", base.ToString()); - } + private string DebuggerDisplay => $"[UserGroup: Id={Id.ToInvariantString()}, Name={Name}]"; + } } \ No newline at end of file diff --git a/redmine-net20-api/Types/UserStatus.cs b/src/redmine-net-api/Types/UserStatus.cs old mode 100755 new mode 100644 similarity index 80% rename from redmine-net20-api/Types/UserStatus.cs rename to src/redmine-net-api/Types/UserStatus.cs index cc5bb5dd..c14b6a38 --- a/redmine-net20-api/Types/UserStatus.cs +++ b/src/redmine-net-api/Types/UserStatus.cs @@ -1,5 +1,5 @@ ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -24,18 +24,14 @@ public enum UserStatus /// /// /// - STATUS_ANONYMOUS = 0, + StatusActive = 1, /// /// /// - STATUS_ACTIVE = 1, + StatusRegistered = 2, /// /// /// - STATUS_REGISTERED = 2, - /// - /// - /// - STATUS_LOCKED = 3 + StatusLocked = 3 } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs new file mode 100644 index 00000000..abe37fe7 --- /dev/null +++ b/src/redmine-net-api/Types/Version.cs @@ -0,0 +1,327 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// Availability 1.3 + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.VERSION)] + public sealed class Version : IdentifiableName, IEquatable + { + #region Properties + /// + /// Gets the project. + /// + /// The project. + public IdentifiableName Project { get; internal set; } + + /// + /// Gets or sets the description. + /// + /// The description. + public string Description { get; set; } + + /// + /// Gets or sets the status. + /// + /// The status. + public VersionStatus Status { get; set; } + + /// + /// Gets or sets the due date. + /// + /// The due date. + public DateTime? DueDate { get; set; } + + /// + /// Gets or sets the sharing. + /// + /// The sharing. + public VersionSharing Sharing { get; set; } + + /// + /// + /// + public string WikiPageTitle { get; set; } + + /// + /// + /// + public float? EstimatedHours { get; set; } + + /// + /// + /// + public float? SpentHours { get; set; } + + /// + /// Gets the created on. + /// + /// The created on. + public DateTime? CreatedOn { get; internal set; } + + /// + /// Gets the updated on. + /// + /// The updated on. + public DateTime? UpdatedOn { get; internal set; } + + /// + /// Gets the custom fields. + /// + /// The custom fields. + public List CustomFields { get; internal set; } + #endregion + + #region Implementation of IXmlSerializable + /// + /// + /// + /// + public override void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; + case RedmineKeys.DUE_DATE: DueDate = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; + case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; + case RedmineKeys.SHARING: Sharing = +#if NETFRAMEWORK + (VersionSharing)Enum.Parse(typeof(VersionSharing), reader.ReadElementContentAsString(), true); break; +#else + Enum.Parse(reader.ReadElementContentAsString(), true); break; +#endif + case RedmineKeys.STATUS: Status = +#if NETFRAMEWORK + (VersionStatus)Enum.Parse(typeof(VersionStatus), reader.ReadElementContentAsString(), true); break; +#else + Enum.Parse(reader.ReadElementContentAsString(), true); break; +#endif + case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.WIKI_PAGE_TITLE: WikiPageTitle = reader.ReadElementContentAsString(); break; + case RedmineKeys.ESTIMATED_HOURS: EstimatedHours = reader.ReadElementContentAsNullableFloat(); break; + case RedmineKeys.SPENT_HOURS: SpentHours = reader.ReadElementContentAsNullableFloat(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteXml(XmlWriter writer) + { + writer.WriteElementString(RedmineKeys.NAME, Name); + writer.WriteElementString(RedmineKeys.STATUS, Status.ToLowerName()); + if (Sharing != VersionSharing.Unknown) + { + writer.WriteElementString(RedmineKeys.SHARING, Sharing.ToLowerName()); + } + + writer.WriteDateOrEmpty(RedmineKeys.DUE_DATE, DueDate); + writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); + writer.WriteElementString(RedmineKeys.WIKI_PAGE_TITLE, WikiPageTitle); + if (CustomFields != null) + { + writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); + } + } + #endregion + + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadAsCollection(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadAsString(); break; + case RedmineKeys.DUE_DATE: DueDate = reader.ReadAsDateTime(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; + case RedmineKeys.SHARING: Sharing = +#if NETFRAMEWORK + (VersionSharing)Enum.Parse(typeof(VersionSharing), reader.ReadAsString() ?? string.Empty, true); break; +#else + Enum.Parse(reader.ReadAsString() ?? string.Empty, true); break; +#endif + case RedmineKeys.STATUS: Status = +#if NETFRAMEWORK + (VersionStatus)Enum.Parse(typeof(VersionStatus), reader.ReadAsString() ?? string.Empty, true); break; +#else + Enum.Parse(reader.ReadAsString() ?? string.Empty, true); break; +#endif + case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.WIKI_PAGE_TITLE: WikiPageTitle = reader.ReadAsString(); break; + case RedmineKeys.ESTIMATED_HOURS: EstimatedHours = (float?)reader.ReadAsDouble(); break; + case RedmineKeys.SPENT_HOURS: SpentHours = (float?)reader.ReadAsDouble(); break; + + + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.VERSION)) + { + writer.WriteProperty(RedmineKeys.NAME, Name); + writer.WriteProperty(RedmineKeys.STATUS, Status.ToLowerName()); + writer.WriteProperty(RedmineKeys.SHARING, Sharing.ToLowerName()); + writer.WriteProperty(RedmineKeys.DESCRIPTION, Description); + writer.WriteDateOrEmpty(RedmineKeys.DUE_DATE, DueDate); + if (CustomFields != null) + { + writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); + } + + writer.WriteProperty(RedmineKeys.WIKI_PAGE_TITLE, WikiPageTitle); + } + } + #endregion + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public bool Equals(Version other) + { + if (other == null) return false; + return base.Equals(other) + && Project == other.Project + && string.Equals(Description, other.Description, StringComparison.Ordinal) + && Status == other.Status + && DueDate == other.DueDate + && Sharing == other.Sharing + && CreatedOn == other.CreatedOn + && UpdatedOn == other.UpdatedOn + && (CustomFields?.Equals(other.CustomFields) ?? other.CustomFields == null) + && string.Equals(WikiPageTitle,other.WikiPageTitle, StringComparison.Ordinal) + && EstimatedHours == other.EstimatedHours + && SpentHours == other.SpentHours; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as Version); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Project, hashCode); + hashCode = HashCodeHelper.GetHashCode(Description, hashCode); + hashCode = HashCodeHelper.GetHashCode(Status, hashCode); + hashCode = HashCodeHelper.GetHashCode(DueDate, hashCode); + hashCode = HashCodeHelper.GetHashCode(Sharing, hashCode); + hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); + hashCode = HashCodeHelper.GetHashCode(WikiPageTitle, hashCode); + hashCode = HashCodeHelper.GetHashCode(EstimatedHours, hashCode); + hashCode = HashCodeHelper.GetHashCode(SpentHours, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Version left, Version right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Version left, Version right) + { + return !Equals(left, right); + } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[Version: Id={Id.ToInvariantString()}, Name={Name}, Status={Status:G}]"; + + } +} diff --git a/redmine-net20-api/Logging/LoggingEventType.cs b/src/redmine-net-api/Types/VersionSharing.cs old mode 100755 new mode 100644 similarity index 71% rename from redmine-net20-api/Logging/LoggingEventType.cs rename to src/redmine-net-api/Types/VersionSharing.cs index 4c30fb4c..d68e8236 --- a/redmine-net20-api/Logging/LoggingEventType.cs +++ b/src/redmine-net-api/Types/VersionSharing.cs @@ -1,5 +1,5 @@ ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,32 +14,36 @@ You may obtain a copy of the License at limitations under the License. */ -namespace Redmine.Net.Api.Logging +namespace Redmine.Net.Api.Types { /// /// /// - public enum LoggingEventType + public enum VersionSharing { /// - /// The debug + /// /// - Debug, + Unknown = 0, /// - /// The information + /// /// - Information, + None = 1, /// - /// The warning + /// /// - Warning, + Descendants, /// - /// The error + /// /// - Error, + Hierarchy, /// - /// The fatal + /// /// - Fatal - }; + Tree, + /// + /// + /// + System + } } \ No newline at end of file diff --git a/redmine-net20-api/Types/PaginatedObjects.cs b/src/redmine-net-api/Types/VersionStatus.cs old mode 100755 new mode 100644 similarity index 74% rename from redmine-net20-api/Types/PaginatedObjects.cs rename to src/redmine-net-api/Types/VersionStatus.cs index 39ffad27..69c42ce9 --- a/redmine-net20-api/Types/PaginatedObjects.cs +++ b/src/redmine-net-api/Types/VersionStatus.cs @@ -1,5 +1,5 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. +ο»Ώ/* + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,27 +14,28 @@ You may obtain a copy of the License at limitations under the License. */ -using System.Collections.Generic; - namespace Redmine.Net.Api.Types { /// /// /// - /// - public class PaginatedObjects + public enum VersionStatus { + /// + /// value of zero - Not set/unknown + /// + None, /// /// /// - public List Objects { get; set; } + Open = 1, /// /// /// - public int TotalCount { get; set; } + Locked, /// /// /// - public int Offset { get; set; } + Closed } } \ No newline at end of file diff --git a/redmine-net20-api/Types/Identifiable.cs b/src/redmine-net-api/Types/Watcher.cs old mode 100755 new mode 100644 similarity index 57% rename from redmine-net20-api/Types/Identifiable.cs rename to src/redmine-net-api/Types/Watcher.cs index 706b2a43..57607455 --- a/redmine-net20-api/Types/Identifiable.cs +++ b/src/redmine-net-api/Types/Watcher.cs @@ -1,5 +1,5 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. +/* + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,7 +15,10 @@ limitations under the License. */ using System; +using System.Diagnostics; using System.Xml.Serialization; +using Redmine.Net.Api.Common; +using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; namespace Redmine.Net.Api.Types @@ -23,27 +26,54 @@ namespace Redmine.Net.Api.Types /// /// /// - /// - public abstract class Identifiable where T : Identifiable, IEquatable + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.USER)] + public sealed class Watcher : IdentifiableName + ,IEquatable + ,ICloneable + ,IValue { + #region Implementation of IValue + /// + /// + /// + public string Value => Id.ToInvariantString(); + #endregion + + #region Implementation of ICloneable /// - /// Gets or sets the id. + /// /// - /// The id. - [XmlAttribute(RedmineKeys.ID)] - public int Id { get; set; } + /// + public new Watcher Clone(bool resetId) + { + if (resetId) + { + return new Watcher() + { + Name = Name + }; + } + return new Watcher + { + Id = Id, + Name = Name + }; + } + + #endregion + #region Implementation of IEquatable /// /// /// /// /// - public bool Equals(Identifiable other) + public bool Equals(Watcher other) { if (other == null) return false; - - return Id == other.Id; + return Id == other.Id && string.Equals(Name, other.Name, StringComparison.Ordinal); } /// @@ -56,30 +86,27 @@ public override bool Equals(object obj) if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != GetType()) return false; - return Equals(obj as Identifiable); + return Equals(obj as Watcher); } - + /// /// /// /// public override int GetHashCode() { - unchecked - { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - return hashCode; - } + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + return hashCode; } - + /// /// /// /// /// /// - public static bool operator ==(Identifiable left, Identifiable right) + public static bool operator ==(Watcher left, Watcher right) { return Equals(left, right); } @@ -90,18 +117,16 @@ public override int GetHashCode() /// /// /// - public static bool operator !=(Identifiable left, Identifiable right) + public static bool operator !=(Watcher left, Watcher right) { return !Equals(left, right); } - + #endregion + /// /// /// /// - public override string ToString() - { - return string.Format("[Identifiable: Id={0}]", Id); - } + private string DebuggerDisplay => $"[{nameof(Watcher)}: {ToString()}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/WikiPage.cs b/src/redmine-net-api/Types/WikiPage.cs new file mode 100644 index 00000000..b81461bd --- /dev/null +++ b/src/redmine-net-api/Types/WikiPage.cs @@ -0,0 +1,291 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// Availability 2.2 + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.WIKI_PAGE)] + public sealed class WikiPage : Identifiable + { + #region Properties + /// + /// Gets the title. + /// + public string Title { get; internal set; } + + /// + /// + /// + public string ParentTitle { get; internal set; } + + /// + /// Gets or sets the text. + /// + public string Text { get; set; } + + /// + /// Gets or sets the comments + /// + public string Comments { get; set; } + + /// + /// Gets or sets the version + /// + public int Version { get; set; } + + /// + /// Gets the author. + /// + public IdentifiableName Author { get; internal set; } + + /// + /// Gets the created on. + /// + /// The created on. + public DateTime? CreatedOn { get; internal set; } + + /// + /// Gets or sets the updated on. + /// + /// The updated on. + public DateTime? UpdatedOn { get; internal set; } + + /// + /// Gets the attachments. + /// + /// + /// The attachments. + /// + public List Attachments { get; set; } + + /// + /// Sets the uploads. + /// + /// + /// The uploads. + /// + /// Availability starting with redmine version 3.3 + public List Uploads { get; set; } + #endregion + + #region Implementation of IXmlSerializable + + /// + /// + /// + /// + public override void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.ATTACHMENTS: Attachments = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; + case RedmineKeys.COMMENTS: Comments = reader.ReadElementContentAsString(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.TEXT: Text = reader.ReadElementContentAsString(); break; + case RedmineKeys.TITLE: Title = reader.ReadElementContentAsString(); break; + case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.VERSION: Version = reader.ReadElementContentAsInt(); break; + case RedmineKeys.PARENT: + { + if (reader.HasAttributes) + { + ParentTitle = reader.GetAttribute(RedmineKeys.TITLE); + reader.Read(); + } + + break; + } + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteXml(XmlWriter writer) + { + writer.WriteElementString(RedmineKeys.TEXT, Text); + writer.WriteElementString(RedmineKeys.COMMENTS, Comments); + writer.WriteValueOrEmpty(RedmineKeys.VERSION, Version); + writer.WriteArray(RedmineKeys.UPLOADS, Uploads); + } + + #endregion + + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.ATTACHMENTS: Attachments = reader.ReadAsCollection(); break; + case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; + case RedmineKeys.COMMENTS: Comments = reader.ReadAsString(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.TEXT: Text = reader.ReadAsString(); break; + case RedmineKeys.TITLE: Title = reader.ReadAsString(); break; + case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.VERSION: Version = reader.ReadAsInt(); break; + case RedmineKeys.PARENT: ParentTitle = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.WIKI_PAGE)) + { + writer.WriteProperty(RedmineKeys.TEXT, Text); + writer.WriteProperty(RedmineKeys.COMMENTS, Comments); + writer.WriteValueOrEmpty(RedmineKeys.VERSION, Version); + writer.WriteArray(RedmineKeys.UPLOADS, Uploads); + } + } + #endregion + + #region Implementation of IEquatable + + /// + /// + /// + /// + /// + public override bool Equals(WikiPage other) + { + if (other == null) return false; + + return base.Equals(other) + && string.Equals(Title, other.Title, StringComparison.Ordinal) + && string.Equals(Text, other.Text, StringComparison.Ordinal) + && string.Equals(Comments, other.Comments, StringComparison.Ordinal) + && string.Equals(ParentTitle, other.ParentTitle, StringComparison.Ordinal) + && Version == other.Version + && Author == other.Author + && CreatedOn == other.CreatedOn + && UpdatedOn == other.UpdatedOn + && (Attachments?.Equals(other.Attachments) ?? other.Attachments == null); + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as WikiPage); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Title, hashCode); + hashCode = HashCodeHelper.GetHashCode(Text, hashCode); + hashCode = HashCodeHelper.GetHashCode(Comments, hashCode); + hashCode = HashCodeHelper.GetHashCode(Version, hashCode); + hashCode = HashCodeHelper.GetHashCode(Author, hashCode); + hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(Attachments, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(WikiPage left, WikiPage right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(WikiPage left, WikiPage right) + { + return !Equals(left, right); + } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[WikiPage: Id={Id.ToInvariantString()}, Title={Title}]"; + } +} \ No newline at end of file diff --git a/redmine-net20-api/Extensions/ExtensionAttribute.cs b/src/redmine-net-api/_net20/ExtensionAttribute.cs similarity index 87% rename from redmine-net20-api/Extensions/ExtensionAttribute.cs rename to src/redmine-net-api/_net20/ExtensionAttribute.cs index c3787c71..4c43dcfc 100755 --- a/redmine-net20-api/Extensions/ExtensionAttribute.cs +++ b/src/redmine-net-api/_net20/ExtensionAttribute.cs @@ -1,5 +1,6 @@ -ο»Ώ/* - Copyright 2011 - 2016 Adrian Popescu +ο»Ώ#if NET20 +/* + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,7 +22,10 @@ namespace System.Runtime.CompilerServices /// /// [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method, AllowMultiple=false, Inherited=false)] - public class ExtensionAttribute: Attribute + public sealed class ExtensionAttribute: Attribute { } -} \ No newline at end of file +} + +#endif + diff --git a/redmine-net20-api/Internals/Func.cs b/src/redmine-net-api/_net20/Func.cs similarity index 95% rename from redmine-net20-api/Internals/Func.cs rename to src/redmine-net-api/_net20/Func.cs index 5f2c21c6..39095ef6 100644 --- a/redmine-net20-api/Internals/Func.cs +++ b/src/redmine-net-api/_net20/Func.cs @@ -1,5 +1,5 @@ ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,7 +14,9 @@ You may obtain a copy of the License at limitations under the License. */ -namespace Redmine.Net.Api.Internals +#if NET20 +// ReSharper disable once CheckNamespace +namespace System { /// /// @@ -66,4 +68,5 @@ namespace Redmine.Net.Api.Internals /// The arg4. /// public delegate TResult Func(T1 arg1, T2 arg2, T3 arg3, T4 arg4); -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api/_net20/IProgress{T}.cs b/src/redmine-net-api/_net20/IProgress{T}.cs new file mode 100644 index 00000000..add86bde --- /dev/null +++ b/src/redmine-net-api/_net20/IProgress{T}.cs @@ -0,0 +1,54 @@ +#if NET20 +/* + Copyright 2011 - 2025 Adrian Popescu + + 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. +*/ +namespace System; + +/// Defines a provider for progress updates. +/// The type of progress update value. +public interface IProgress +{ + /// Reports a progress update. + /// The value of the updated progress. + void Report(T value); +} + +/// +/// +/// +/// +public sealed class Progress : IProgress +{ + private readonly Action _handler; + + /// + /// + /// + /// + public Progress(Action handler) + { + _handler = handler; + } + + /// + /// + /// + /// + public void Report(T value) + { + _handler(value); + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj new file mode 100644 index 00000000..6f6c22a4 --- /dev/null +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -0,0 +1,143 @@ + + + + + |net20|net40| + |net20|net40|net45|net451|net452|net46|net461| + |net45|net451|net452|net46|net461| + + + + Redmine.Net.Api + redmine-net-api + net9.0;net8.0;net7.0;net6.0;net5.0;net481;net48;net472;net471;net47;net462;net461;net46;net452;net451;net45;net40;net20 + false + True + true + TRACE + Debug;Release;DebugJson + PackageReference + + NU5105; + CA1303; + CA1056; + CA1062; + CA1707; + CA1716; + CA1724; + CA1806; + CA2227; + CS0612; + CS0618; + CA1002; + + + NU5105; + CA1303; + CA1056; + CA1062; + CA1707; + CA1716; + CA1724; + CA1806; + CA2227; + CS0612; + CS0618; + CA1002; + SYSLIB0014; + + + + + true + true + AllEnabledByDefault + latest + + + + full + portable + false + $(SolutionDir)/artifacts + + + + Adrian Popescu + Redmine Api is a .NET rest client for Redmine. + p.adi + Adrian Popescu, 2011 - $([System.DateTime]::Now.Year.ToString()) + en-US + redmine-api + redmine-api-signed + https://raw.githubusercontent.com/zapadi/redmine-net-api/master/logo.png + logo.png + LICENSE + Apache-2.0 + https://github.com/zapadi/redmine-net-api + README.md + true + Redmine; REST; API; Client; .NET; Adrian Popescu; + Redmine .NET API Client + git + https://github.com/zapadi/redmine-net-api + Redmine .NET API Client + + true + + true + true + snupkg + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + redmine-net-api.snk + + + + + + + + + <_Parameter1>Padi.DotNet.RedmineAPI.Tests + + + <_Parameter1>Padi.DotNet.RedmineAPI.Integration.Tests + + + + + + + + diff --git a/tests/redmine-net-api.Integration.Tests/Collections/RedmineTestContainerCollection.cs b/tests/redmine-net-api.Integration.Tests/Collections/RedmineTestContainerCollection.cs new file mode 100644 index 00000000..0ca114d1 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Collections/RedmineTestContainerCollection.cs @@ -0,0 +1,6 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; + +[CollectionDefinition(Constants.RedmineTestContainerCollection)] +public sealed class RedmineTestContainerCollection : ICollectionFixture { } \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerFixture.cs b/tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerFixture.cs new file mode 100644 index 00000000..06fc5750 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerFixture.cs @@ -0,0 +1,237 @@ +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Configurations; +using DotNet.Testcontainers.Containers; +using DotNet.Testcontainers.Networks; +using Npgsql; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure.Options; +using Redmine.Net.Api; +using Redmine.Net.Api.Options; +using Testcontainers.PostgreSql; +using Xunit.Abstractions; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; + +public class RedmineTestContainerFixture : IAsyncLifetime +{ + private readonly RedmineConfiguration _configuration; + private readonly string _redmineNetworkAlias = Guid.NewGuid().ToString(); + + private readonly ITestOutputHelper _output; + private readonly TestContainerOptions _testContainerOptions; + + private INetwork Network { get; set; } + private PostgreSqlContainer PostgresContainer { get; set; } + private IContainer RedmineContainer { get; set; } + public RedmineManager RedmineManager { get; private set; } + public string RedmineHost { get; private set; } + + public RedmineTestContainerFixture() + { + //_configuration = configuration; + _testContainerOptions = ConfigurationHelper.GetConfiguration(); + + if (_testContainerOptions.Mode != TestContainerMode.UseExisting) + { + BuildContainers(); + } + } + + /// + /// Detects if running in a CI/CD environment + /// + private static bool IsRunningInCiEnvironment() + { + return !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI")) || + !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITHUB_ACTIONS")); + + } + + private void BuildContainers() + { + Network = new NetworkBuilder() + .WithDriver(NetworkDriver.Bridge) + .Build(); + + var postgresBuilder = new PostgreSqlBuilder() + .WithImage(_testContainerOptions.Postgres.Image) + .WithNetwork(Network) + .WithNetworkAliases(_redmineNetworkAlias) + .WithEnvironment(new Dictionary + { + { "POSTGRES_DB", _testContainerOptions.Postgres.Database }, + { "POSTGRES_USER", _testContainerOptions.Postgres.User }, + { "POSTGRES_PASSWORD", _testContainerOptions.Postgres.Password }, + }) + .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(_testContainerOptions.Postgres.Port)); + + if (_testContainerOptions.Mode == TestContainerMode.CreateNewWithRandomPorts) + { + postgresBuilder.WithPortBinding(_testContainerOptions.Postgres.Port, assignRandomHostPort: true); + } + else + { + postgresBuilder.WithPortBinding(_testContainerOptions.Postgres.Port, _testContainerOptions.Postgres.Port); + } + + PostgresContainer = postgresBuilder.Build(); + + var redmineBuilder = new ContainerBuilder() + .WithImage(_testContainerOptions.Redmine.Image) + .WithNetwork(Network) + .WithEnvironment(new Dictionary + { + { "REDMINE_DB_POSTGRES", _redmineNetworkAlias }, + { "REDMINE_DB_PORT", _testContainerOptions.Redmine.Port.ToString() }, + { "REDMINE_DB_DATABASE", _testContainerOptions.Postgres.Database }, + { "REDMINE_DB_USERNAME", _testContainerOptions.Postgres.User }, + { "REDMINE_DB_PASSWORD", _testContainerOptions.Postgres.Password }, + }) + .DependsOn(PostgresContainer) + .WithWaitStrategy(Wait.ForUnixContainer() + .UntilHttpRequestIsSucceeded(request => request.ForPort((ushort)_testContainerOptions.Redmine.Port).ForPath("/"))); + + if (_testContainerOptions.Mode == TestContainerMode.CreateNewWithRandomPorts) + { + redmineBuilder.WithPortBinding(_testContainerOptions.Redmine.Port, assignRandomHostPort: true); + } + else + { + redmineBuilder.WithPortBinding(_testContainerOptions.Redmine.Port, _testContainerOptions.Redmine.Port); + } + + RedmineContainer = redmineBuilder.Build(); + } + + public async Task InitializeAsync() + { + var rmgBuilder = new RedmineManagerOptionsBuilder(); + + switch (_testContainerOptions.Redmine.AuthenticationMode) + { + case AuthenticationMode.ApiKey: + var apiKey = _testContainerOptions.Redmine.Authentication.ApiKey; + rmgBuilder.WithApiKeyAuthentication(apiKey); + break; + case AuthenticationMode.Basic: + var username = _testContainerOptions.Redmine.Authentication.Basic.Username; + var password = _testContainerOptions.Redmine.Authentication.Basic.Password; + rmgBuilder.WithBasicAuthentication(username, password); + break; + } + + if (_testContainerOptions.Mode == TestContainerMode.UseExisting) + { + RedmineHost = _testContainerOptions.Redmine.Url; + } + else + { + await Network.CreateAsync(); + + await PostgresContainer.StartAsync(); + + await RedmineContainer.StartAsync(); + + await SeedTestDataAsync(PostgresContainer, CancellationToken.None); + + RedmineHost = $"http://{RedmineContainer.Hostname}:{RedmineContainer.GetMappedPublicPort(_testContainerOptions.Redmine.Port)}"; + } + + rmgBuilder.WithHost(RedmineHost); + + if (_configuration != null) + { + switch (_configuration.Client) + { + case ClientType.Http: + rmgBuilder.UseHttpClient(); + break; + case ClientType.Web: + rmgBuilder.UseWebClient(); + break; + } + + switch (_configuration.Serialization) + { + case SerializationType.Xml: + rmgBuilder.WithXmlSerialization(); + break; + case SerializationType.Json: + rmgBuilder.WithJsonSerialization(); + break; + } + } + else + { + rmgBuilder + .UseHttpClient() + // .UseWebClient() + .WithXmlSerialization(); + } + + RedmineManager = new RedmineManager(rmgBuilder); + } + + public async Task DisposeAsync() + { + var exceptions = new List(); + + if (_testContainerOptions.Mode == TestContainerMode.UseExisting) + { + return; + } + + await SafeDisposeAsync(() => RedmineContainer.StopAsync()); + await SafeDisposeAsync(() => PostgresContainer.StopAsync()); + await SafeDisposeAsync(() => Network.DisposeAsync().AsTask()); + + if (exceptions.Count > 0) + { + throw new AggregateException(exceptions); + } + + return; + + async Task SafeDisposeAsync(Func disposeFunc) + { + try + { + await disposeFunc(); + } + catch (Exception ex) + { + exceptions.Add(ex); + } + } + } + + private async Task SeedTestDataAsync(PostgreSqlContainer container, CancellationToken ct) + { + const int maxDbAttempts = 10; + var dbRetryDelay = TimeSpan.FromSeconds(2); + var connectionString = container.GetConnectionString(); + for (var attempt = 1; attempt <= maxDbAttempts; attempt++) + { + try + { + await using var conn = new NpgsqlConnection(connectionString); + await conn.OpenAsync(ct); + break; + } + catch + { + if (attempt == maxDbAttempts) + { + throw; + } + await Task.Delay(dbRetryDelay, ct); + } + } + var sql = await System.IO.File.ReadAllTextAsync(_testContainerOptions.Redmine.SqlFilePath, ct); + var res = await container.ExecScriptAsync(sql, ct); + if (!string.IsNullOrWhiteSpace(res.Stderr)) + { + _output.WriteLine(res.Stderr); + } + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Helpers/AssertHelpers.cs b/tests/redmine-net-api.Integration.Tests/Helpers/AssertHelpers.cs new file mode 100644 index 00000000..d105f53e --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Helpers/AssertHelpers.cs @@ -0,0 +1,31 @@ +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; + +internal static class AssertHelpers +{ + /// + /// Asserts that two values are equal within the specified tolerance. + /// + public static void Equal(float expected, float actual, float tolerance = 1e-4f) + => Assert.InRange(actual, expected - tolerance, expected + tolerance); + + /// + /// Asserts that two values are equal within the specified tolerance. + /// + public static void Equal(decimal expected, decimal actual, decimal tolerance = 0.0001m) + => Assert.InRange(actual, expected - tolerance, expected + tolerance); + + /// + /// Asserts that two values are equal within the supplied tolerance. + /// Kind is ignored – both values are first converted to UTC. + /// + public static void Equal(DateTime expected, DateTime actual, TimeSpan? tolerance = null) + { + tolerance ??= TimeSpan.FromSeconds(1); + + var expectedUtc = expected.ToUniversalTime(); + var actualUtc = actual.ToUniversalTime(); + + Assert.InRange(actualUtc, expectedUtc - tolerance.Value, expectedUtc + tolerance.Value); + } + +} diff --git a/tests/redmine-net-api.Integration.Tests/Helpers/FileGeneratorHelper.cs b/tests/redmine-net-api.Integration.Tests/Helpers/FileGeneratorHelper.cs new file mode 100644 index 00000000..7ec2cc47 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Helpers/FileGeneratorHelper.cs @@ -0,0 +1,142 @@ +using System.Text; +using Redmine.Net.Api; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Types; +using File = System.IO.File; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; + +internal static class FileGeneratorHelper +{ + private static readonly string[] Extensions = [".txt", ".doc", ".pdf", ".xml", ".json"]; + + /// + /// Generates random file content with a specified size. + /// + /// Size of the file in kilobytes. + /// Byte array containing the file content. + public static byte[] GenerateRandomFileBytes(int sizeInKb) + { + var sizeInBytes = sizeInKb * 1024; + var bytes = new byte[sizeInBytes]; + RandomHelper.FillRandomBytes(bytes); + return bytes; + } + + /// + /// Generates a random text file with a specified size. + /// + /// Size of the file in kilobytes. + /// Byte array containing the text file content. + public static byte[] GenerateRandomTextFileBytes(int sizeInKb) + { + var roughCharCount = sizeInKb * 1024; + + var sb = new StringBuilder(roughCharCount); + + while (sb.Length < roughCharCount) + { + sb.AppendLine(RandomHelper.GenerateText(RandomHelper.GetRandomNumber(5, 80))); + } + + var text = sb.ToString(); + + if (text.Length > roughCharCount) + { + text = text[..roughCharCount]; + } + + return Encoding.UTF8.GetBytes(text); + } + + /// + /// Creates a random file with a specified size and returns its path. + /// + /// Size of the file in kilobytes. + /// If true, generates text content; otherwise, generates binary content. + /// Path to the created temporary file. + public static string CreateRandomFile(int sizeInKb, bool useTextContent = true) + { + var extension = Extensions[RandomHelper.GetRandomNumber(Extensions.Length)]; + var fileName = RandomHelper.GenerateText("test-file", 7); + var filePath = Path.Combine(Path.GetTempPath(), $"{fileName}{extension}"); + + var content = useTextContent + ? GenerateRandomTextFileBytes(sizeInKb) + : GenerateRandomFileBytes(sizeInKb); + + File.WriteAllBytes(filePath, content); + return filePath; + } + +} + +internal static class FileTestHelper +{ + private static (string fileNameame, byte[] fileContent) GenerateFile(int sizeInKb) + { + var fileName = RandomHelper.GenerateText("test-file", 7); + var fileContent = sizeInKb >= 1024 + ? FileGeneratorHelper.GenerateRandomTextFileBytes(sizeInKb) + : FileGeneratorHelper.GenerateRandomFileBytes(sizeInKb); + + return (fileName, fileContent); + } + public static Upload UploadRandomFile(IRedmineManager client, int sizeInKb, RequestOptions options = null) + { + var (fileName, fileContent) = GenerateFile(sizeInKb); + return client.UploadFile(fileContent, fileName); + } + + /// + /// Helper method to upload a 500KB file. + /// + /// The Redmine API client. + /// Request options. + /// API response message containing the uploaded file information. + public static Upload UploadRandom500KbFile(IRedmineManager client, RequestOptions options = null) + { + return UploadRandomFile(client, 500, options); + } + + /// + /// Helper method to upload a 1MB file. + /// + /// The Redmine API client. + /// Request options. + /// API response message containing the uploaded file information. + public static Upload UploadRandom1MbFile(IRedmineManager client, RequestOptions options = null) + { + return UploadRandomFile(client, 1024, options); + } + + public static async Task UploadRandomFileAsync(IRedmineManagerAsync client, int sizeInKb, RequestOptions options = null) + { + var (fileName, fileContent) = GenerateFile(sizeInKb); + + return await client.UploadFileAsync(fileContent, fileName, options); + } + + /// + /// Helper method to upload a 500KB file. + /// + /// The Redmine API client. + /// Request options. + /// API response message containing the uploaded file information. + public static Task UploadRandom500KbFileAsync(IRedmineManagerAsync client, RequestOptions options = null) + { + return UploadRandomFileAsync(client, 500, options); + } + + /// + /// Helper method to upload a 1MB file. + /// + /// The Redmine API client. + /// Request options. + /// API response message containing the uploaded file information. + public static Task UploadRandom1MbFileAsync(IRedmineManagerAsync client, RequestOptions options = null) + { + return UploadRandomFileAsync(client, 1024, options); + } + +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Helpers/RandomHelper.cs b/tests/redmine-net-api.Integration.Tests/Helpers/RandomHelper.cs new file mode 100644 index 00000000..ff4923b5 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Helpers/RandomHelper.cs @@ -0,0 +1,218 @@ +using System.Text; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; + +internal static class RandomHelper +{ + /// + /// Generates a cryptographically strong, random string suffix. + /// This method is thread-safe as Guid.NewGuid() is thread-safe. + /// + /// A random string, 32 characters long, consisting of hexadecimal characters, without hyphens. + private static string GenerateSuffix() + { + return Guid.NewGuid().ToString("N"); + } + + private static readonly char[] EnglishAlphabetChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + .ToCharArray(); + + // ThreadLocal ensures that each thread has its own instance of Random, + // which is important because System.Random is not thread-safe for concurrent use. + // Seed with Guid for better randomness across instances + private static readonly ThreadLocal ThreadRandom = + new ThreadLocal(() => new Random(Guid.NewGuid().GetHashCode())); + + /// + /// Generates a random string of a specified length using only English alphabet characters. + /// This method is thread-safe. + /// + /// The desired length of the random string. Defaults to 10. + /// A random string composed of English alphabet characters. + private static string GenerateRandomString(int length = 10) + { + if (length <= 0) + { + throw new ArgumentOutOfRangeException(nameof(length), "Length must be a positive integer."); + } + + var random = ThreadRandom.Value; + var result = new StringBuilder(length); + for (var i = 0; i < length; i++) + { + result.Append(EnglishAlphabetChars[random.Next(EnglishAlphabetChars.Length)]); + } + + return result.ToString(); + } + + internal static void FillRandomBytes(byte[] bytes) + { + ThreadRandom.Value.NextBytes(bytes); + } + + internal static int GetRandomNumber(int max) + { + return ThreadRandom.Value.Next(max); + } + + internal static int GetRandomNumber(int min, int max) + { + return ThreadRandom.Value.Next(min, max); + } + + /// + /// Generates a random alphabetic suffix, defaulting to 10 characters. + /// This method is thread-safe. + /// + /// The desired length of the suffix. Defaults to 10. + /// A random alphabetic string. + public static string GenerateText(int length = 10) + { + return GenerateRandomString(length); + } + + /// + /// Generates a random name by combining a specified prefix and a random alphabetic suffix. + /// This method is thread-safe. + /// Example: if the prefix is "MyItem", the result could be "MyItem_aBcDeFgHiJ". + /// + /// The prefix for the name. A '_' separator will be added. + /// The desired length of the random suffix. Defaults to 10. + /// A string combining the prefix, an underscore, and a random alphabetic suffix. + /// If the prefix is null or empty, it returns just the random suffix. + public static string GenerateText(string prefix = null, int suffixLength = 10) + { + var suffix = GenerateRandomString(suffixLength); + return string.IsNullOrEmpty(prefix) ? suffix : $"{prefix}_{suffix}"; + } + + /// + /// Generates a random email address with alphabetic characters only. + /// + /// Length of the local part (before @). Defaults to 8. + /// Length of the domain name (without extension). Defaults to 6. + /// A random email address with only alphabetic characters. + public static string GenerateEmail(int localPartLength = 8, int domainLength = 6) + { + if (localPartLength <= 0 || domainLength <= 0) + { + throw new ArgumentOutOfRangeException( + localPartLength <= 0 ? nameof(localPartLength) : nameof(domainLength), + "Length must be a positive integer."); + } + + var localPart = GenerateRandomString(localPartLength); + var domain = GenerateRandomString(domainLength).ToLower(); + + // Use common TLDs + var tlds = new[] { "com", "org", "net", "io" }; + var tld = tlds[ThreadRandom.Value.Next(tlds.Length)]; + + return $"{localPart}@{domain}.{tld}"; + } + + /// + /// Generates a random webpage URL with alphabetic characters only. + /// + /// Length of the domain name (without extension). Defaults to 8. + /// Length of the path segment. Defaults to 10. + /// A random webpage URL with only alphabetic characters. + public static string GenerateWebpage(int domainLength = 8, int pathLength = 10) + { + if (domainLength <= 0 || pathLength <= 0) + { + throw new ArgumentOutOfRangeException( + domainLength <= 0 ? nameof(domainLength) : nameof(pathLength), + "Length must be a positive integer."); + } + + var domain = GenerateRandomString(domainLength).ToLower(); + + // Use common TLDs + var tlds = new[] { "com", "org", "net", "io" }; + var tld = tlds[ThreadRandom.Value.Next(tlds.Length)]; + + // Generate path segments + var segments = ThreadRandom.Value.Next(0, 3); + var path = ""; + + if (segments > 0) + { + var pathSegments = new List(segments); + for (int i = 0; i < segments; i++) + { + pathSegments.Add(GenerateRandomString(ThreadRandom.Value.Next(3, pathLength)).ToLower()); + } + + path = "/" + string.Join("/", pathSegments); + } + + return $"https://www.{domain}.{tld}{path}/"; + } + + /// + /// Generates a random name composed only of alphabetic characters from the English alphabet. + /// + /// Length of the name. Defaults to 6. + /// Whether to capitalize the first letter. Defaults to true. + /// A random name with only English alphabetic characters. + public static string GenerateName(int length = 6, bool capitalize = true) + { + if (length <= 0) + { + throw new ArgumentOutOfRangeException(nameof(length), "Length must be a positive integer."); + } + + // Generate random name + var name = GenerateRandomString(length); + + if (capitalize) + { + name = char.ToUpper(name[0]) + name.Substring(1).ToLower(); + } + else + { + name = name.ToLower(); + } + + return name; + } + + /// + /// Generates a random full name composed only of alphabetic characters. + /// + /// Length of the first name. Defaults to 6. + /// Length of the last name. Defaults to 8. + /// A random full name with only alphabetic characters. + public static string GenerateFullName(int firstNameLength = 6, int lastNameLength = 8) + { + if (firstNameLength <= 0 || lastNameLength <= 0) + { + throw new ArgumentOutOfRangeException( + firstNameLength <= 0 ? nameof(firstNameLength) : nameof(lastNameLength), + "Length must be a positive integer."); + } + + // Generate random first and last names using the new alphabetic-only method + var firstName = GenerateName(firstNameLength); + var lastName = GenerateName(lastNameLength); + + return $"{firstName} {lastName}"; + } + + // Fisher-Yates shuffle algorithm + public static void Shuffle(this List list) + { + var n = list.Count; + var random = ThreadRandom.Value; + while (n > 1) + { + n--; + var k = random.Next(n + 1); + var value = list[k]; + list[k] = list[n]; + list[n] = value; + } + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/ClientType.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/ClientType.cs new file mode 100644 index 00000000..62dff405 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/ClientType.cs @@ -0,0 +1,7 @@ +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; + +public enum ClientType +{ + Http, + Web +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/ConfigurationHelper.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/ConfigurationHelper.cs new file mode 100644 index 00000000..8d1214f3 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/ConfigurationHelper.cs @@ -0,0 +1,38 @@ +ο»Ώusing Microsoft.Extensions.Configuration; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure.Options; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure +{ + internal static class ConfigurationHelper + { + private static IConfigurationRoot GetIConfigurationRoot(string outputPath) + { + // var environment = Environment.GetEnvironmentVariable("Environment"); + + return new ConfigurationBuilder() + .SetBasePath(outputPath) + .AddJsonFile("appsettings.json", optional: true) + // .AddJsonFile($"appsettings.{environment}.json", optional: true) + .AddJsonFile($"appsettings.local.json", optional: true) + // .AddUserSecrets("f8b9e946-b547-42f1-861c-f719dca00a84") + .Build(); + } + + public static TestContainerOptions GetConfiguration(string outputPath = "") + { + if (string.IsNullOrWhiteSpace(outputPath)) + { + outputPath = Directory.GetCurrentDirectory(); + } + + var testContainerOptions = new TestContainerOptions(); + + var iConfig = GetIConfigurationRoot(outputPath); + + iConfig.GetSection("TestContainer") + .Bind(testContainerOptions); + + return testContainerOptions; + } + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/Constants.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/Constants.cs new file mode 100644 index 00000000..85806f08 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/Constants.cs @@ -0,0 +1,6 @@ +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; + +public static class Constants +{ + public const string RedmineTestContainerCollection = nameof(RedmineTestContainerCollection); +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/AuthenticationMode.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/AuthenticationMode.cs new file mode 100644 index 00000000..45b5d786 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/AuthenticationMode.cs @@ -0,0 +1,8 @@ +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure.Options; + +public enum AuthenticationMode +{ + None, + ApiKey, + Basic +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/AuthenticationOptions.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/AuthenticationOptions.cs new file mode 100644 index 00000000..bed34dff --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/AuthenticationOptions.cs @@ -0,0 +1,8 @@ +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure.Options; + +public sealed class AuthenticationOptions +{ + public string ApiKey { get; set; } + + public BasicAuthenticationOptions Basic { get; set; } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/BasicAuthenticationOptions.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/BasicAuthenticationOptions.cs new file mode 100644 index 00000000..9aa9f28a --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/BasicAuthenticationOptions.cs @@ -0,0 +1,7 @@ +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure.Options; + +public sealed class BasicAuthenticationOptions +{ + public string Username { get; set; } + public string Password { get; set; } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/PostgresOptions.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/PostgresOptions.cs new file mode 100644 index 00000000..77093b4c --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/PostgresOptions.cs @@ -0,0 +1,10 @@ +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure.Options; + +public sealed class PostgresOptions +{ + public int Port { get; set; } + public string Image { get; set; } = string.Empty; + public string Database { get; set; } = string.Empty; + public string User { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/RedmineOptions.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/RedmineOptions.cs new file mode 100644 index 00000000..8e7cb6b2 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/RedmineOptions.cs @@ -0,0 +1,15 @@ +ο»Ώnamespace Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure.Options +{ + public sealed class RedmineOptions + { + public string Url { get; set; } + + public AuthenticationMode AuthenticationMode { get; set; } + + public AuthenticationOptions Authentication { get; set; } + + public int Port { get; set; } + public string Image { get; set; } = string.Empty; + public string SqlFilePath { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/TestContainerOptions.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/TestContainerOptions.cs new file mode 100644 index 00000000..c26821c6 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/TestContainerOptions.cs @@ -0,0 +1,10 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure.Options; + +public sealed class TestContainerOptions +{ + public RedmineOptions Redmine { get; set; } + public PostgresOptions Postgres { get; set; } + public TestContainerMode Mode { get; set; } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/RedmineConfiguration.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/RedmineConfiguration.cs new file mode 100644 index 00000000..4a82568a --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/RedmineConfiguration.cs @@ -0,0 +1,3 @@ +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; + +public record RedmineConfiguration(SerializationType Serialization, ClientType Client); \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/SerializationType.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/SerializationType.cs new file mode 100644 index 00000000..8ba1ed61 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/SerializationType.cs @@ -0,0 +1,7 @@ +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; + +public enum SerializationType +{ + Xml, + Json +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/TestContainerMode.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/TestContainerMode.cs new file mode 100644 index 00000000..03a2443a --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/TestContainerMode.cs @@ -0,0 +1,13 @@ +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; + +/// +/// Enum defining how containers should be managed +/// +public enum TestContainerMode +{ + /// Use existing running containers at specified URL + UseExisting, + + /// Create new containers with random ports (CI-friendly) + CreateNewWithRandomPorts +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/TestData/init-redmine.sql b/tests/redmine-net-api.Integration.Tests/TestData/init-redmine.sql new file mode 100644 index 00000000..85fabbf1 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/TestData/init-redmine.sql @@ -0,0 +1,70 @@ +-- 1. Insert users +INSERT INTO users (id, login, hashed_password, salt, firstname, lastname, admin, status, type, created_on, updated_on) +VALUES (90, 'adminuser', '5cfe86e41de3a143be90ae5f7ced76841a0830bf', 'e71a2bcb922bede1becc396b326b93ff', 'Admin', 'User', true, 1, 'User', NOW(), NOW()), + (91, 'normaluser', '3c4afd1d5042356c7fdd19e0527db108919624f9', '6030b2ed3c7eb797eb706a325bb227ad', 'Normal', 'User', false, 1, 'User', NOW(), NOW()); + +-- 2. Insert API keys +INSERT INTO tokens (user_id, action, value, created_on) +VALUES + (90, 'api', '029a9d38-17e8-41ae-bc8c-fbf71e193c57', NOW()), + (91, 'api', 'b94da108-c6d0-483a-9c21-2648fe54521d', NOW()); + +INSERT INTO settings (id, name, "value", updated_on) +values (99, 'rest_api_enabled', 1, now()); + +insert into enabled_modules (id, project_id, name) +values (1, 1, 'issue_tracking'), + (2, 1, 'time_tracking'), + (3, 1, 'news'), + (4, 1, 'documents'), + (5, 1, 'files'), + (6, 1, 'wiki'), + (7, 1, 'repository'), + (8, 1, 'boards'), + (9, 1, 'calendar'), + (10, 1, 'gantt'); + + +insert into enumerations (id, name, position, is_default, type, active, project_id, parent_id, position_name) +values (1, 'Low', 1, false, 'IssuePriority', true, null, null, 'lowest'), + (2, 'Normal', 2, true, 'IssuePriority', true, null, null, 'default'), + (3, 'High', 3, false, 'IssuePriority', true, null, null, 'high3'), + (4, 'Urgent', 4, false, 'IssuePriority', true, null, null, 'high2'), + (5, 'Immediate', 5, false, 'IssuePriority', true, null, null, 'highest'), + (6, 'User documentation', 1, false, 'DocumentCategory', true, null, null, null), + (7, 'Technical documentation', 2, false, 'DocumentCategory', true, null, null, null), + (8, 'Design', 1, false, 'TimeEntryActivity', true, null, null, null), + (9, 'Development', 2, false, 'TimeEntryActivity', true, null, null, null); + +insert into issue_statuses (id, name, is_closed, position, default_done_ratio, description) +values (1, 'New', false, 1, null, null), + (2, 'In Progress', false, 2, null, null), + (3, 'Resolved', false, 3, null, null), + (4, 'Feedback', false, 4, null, null), + (5, 'Closed', true, 5, null, null), + (6, 'Rejected', true, 6, null, null); + + +insert into trackers (id, name, position, is_in_roadmap, fields_bits, default_status_id, description) +values (1, 'Bug', 1, false, 0, 1, null), + (2, 'Feature', 2, true, 0, 1, null), + (3, 'Support', 3, false, 0, 1, null); + +insert into projects (id, name, description, homepage, is_public, parent_id, created_on, updated_on, identifier, status, lft, rgt, inherit_members, default_version_id, default_assigned_to_id, default_issue_query_id) +values (1, 'Project-Test', null, '', true, null, '2024-09-02 10:14:33.789394', '2024-09-02 10:14:33.789394', 'project-test', 1, 1, 2, false, null, null, null); + +insert into public.wikis (id, project_id, start_page, status) values (1, 1, 'Wiki', 1); + +insert into versions (id, project_id, name, description, effective_date, created_on, updated_on, wiki_page_title, status, sharing) +values (1, 1, 'version1', '', null, '2025-04-28 17:56:49.245993', '2025-04-28 17:56:49.245993', '', 'open', 'none'), + (2, 1, 'version2', '', null, '2025-04-28 17:57:05.138915', '2025-04-28 17:57:05.138915', '', 'open', 'descendants'); + +insert into issues (id, tracker_id, project_id, subject, description, due_date, category_id, status_id, assigned_to_id, priority_id, fixed_version_id, author_id, lock_version, created_on, updated_on, start_date, done_ratio, estimated_hours, parent_id, root_id, lft, rgt, is_private, closed_on) +values (5, 1, 1, '#380', '', null, 1, 1, null, 2, 2, 90, 1, '2025-04-28 17:58:42.818731', '2025-04-28 17:58:42.818731', '2025-04-28', 0, null, null, 5, 1, 2, false, null), + (6, 1, 1, 'issue with file', '', null, null, 1, null, 3, 2, 90, 1, '2025-04-28 18:00:07.296872', '2025-04-28 18:00:07.296872', '2025-04-28', 0, null, null, 6, 1, 2, false, null); + +insert into watchers (id, watchable_type, watchable_id, user_id) +values (8, 'Issue', 5, 90), + (9, 'Issue', 5, 91); + + diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Common/EmailNotificationType.cs b/tests/redmine-net-api.Integration.Tests/Tests/Common/EmailNotificationType.cs new file mode 100644 index 00000000..e77fef76 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Common/EmailNotificationType.cs @@ -0,0 +1,29 @@ +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; + +public sealed record EmailNotificationType +{ + public static readonly EmailNotificationType OnlyMyEvents = new EmailNotificationType(1, "only_my_events"); + public static readonly EmailNotificationType OnlyAssigned = new EmailNotificationType(2, "only_assigned"); + public static readonly EmailNotificationType OnlyOwner = new EmailNotificationType(3, "only_owner"); + public static readonly EmailNotificationType None = new EmailNotificationType(0, ""); + + public int Id { get; } + public string Name { get; } + + private EmailNotificationType(int id, string name) + { + Id = id; + Name = name; + } + + public static EmailNotificationType FromId(int id) + { + return id switch + { + 1 => OnlyMyEvents, + 2 => OnlyAssigned, + 3 => OnlyOwner, + _ => None + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Common/IssueTestHelper.cs b/tests/redmine-net-api.Integration.Tests/Tests/Common/IssueTestHelper.cs new file mode 100644 index 00000000..fe1fa744 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Common/IssueTestHelper.cs @@ -0,0 +1,82 @@ +using Redmine.Net.Api; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; + +internal static class IssueTestHelper +{ + internal static void AssertBasic(Issue expected, Issue actual) + { + Assert.NotNull(actual); + Assert.True(actual.Id > 0); + Assert.Equal(expected.Subject, actual.Subject); + Assert.Equal(expected.Description, actual.Description); + Assert.Equal(expected.Project.Id, actual.Project.Id); + Assert.Equal(expected.Tracker.Id, actual.Tracker.Id); + Assert.Equal(expected.Status.Id, actual.Status.Id); + Assert.Equal(expected.Priority.Id, actual.Priority.Id); + } + + internal static (Issue, Issue payload) CreateRandomIssue(RedmineManager redmineManager, int projectId = TestConstants.Projects.DefaultProjectId, + int trackerId = 1, + int priorityId = 2, + int statusId = 1, + string subject = null, + List customFields = null, + List watchers = null, + List uploads = null) + { + var issuePayload = TestEntityFactory.CreateRandomIssuePayload(projectId, trackerId, priorityId, statusId, + subject, customFields, watchers, uploads); + var issue = redmineManager.Create(issuePayload); + Assert.NotNull(issue); + return (issue, issuePayload); + } + + internal static async Task<(Issue, Issue payload)> CreateRandomIssueAsync(RedmineManager redmineManager, int projectId = TestConstants.Projects.DefaultProjectId, + int trackerId = 1, + int priorityId = 2, + int statusId = 1, + string subject = null, + List customFields = null, + List watchers = null, + List uploads = null) + { + var issuePayload = TestEntityFactory.CreateRandomIssuePayload(projectId, trackerId, priorityId, statusId, + subject, customFields, watchers, uploads); + var issue = await redmineManager.CreateAsync(issuePayload); + Assert.NotNull(issue); + return (issue, issuePayload); + } + + public static (Issue first, Issue second) CreateRandomTwoIssues(RedmineManager redmineManager) + { + return (Build(), Build()); + + Issue Build() => redmineManager.Create(TestEntityFactory.CreateRandomIssuePayload()); + } + + public static (IssueRelation issueRelation, Issue firstIssue, Issue secondIssue) CreateRandomIssueRelation(RedmineManager redmineManager, IssueRelationType issueRelationType = IssueRelationType.Relates) + { + var (i1, i2) = CreateRandomTwoIssues(redmineManager); + var rel = TestEntityFactory.CreateRandomIssueRelationPayload(i1.Id, i2.Id, issueRelationType); + var relation = redmineManager.Create(rel, i1.Id.ToString()); + return (relation, i1, i2); + } + + public static async Task<(Issue first, Issue second)> CreateRandomTwoIssuesAsync(RedmineManager redmineManager) + { + return (await BuildAsync(), await BuildAsync()); + + async Task BuildAsync() => await redmineManager.CreateAsync(TestEntityFactory.CreateRandomIssuePayload()); + } + + public static async Task<(IssueRelation issueRelation, Issue firstIssue, Issue secondIssue)> CreateRandomIssueRelationAsync(RedmineManager redmineManager, IssueRelationType issueRelationType = IssueRelationType.Relates) + { + var (i1, i2) = await CreateRandomTwoIssuesAsync(redmineManager); + var rel = TestEntityFactory.CreateRandomIssueRelationPayload(i1.Id, i2.Id, issueRelationType); + var relation = redmineManager.Create(rel, i1.Id.ToString()); + return (relation, i1, i2); + } + +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Common/TestConstants.cs b/tests/redmine-net-api.Integration.Tests/Tests/Common/TestConstants.cs new file mode 100644 index 00000000..12c4f636 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Common/TestConstants.cs @@ -0,0 +1,20 @@ +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; + +public static class TestConstants +{ + public static class Projects + { + public const int DefaultProjectId = 1; + public const string DefaultProjectIdentifier = "1"; + public static readonly IdentifiableName DefaultProject = DefaultProject.ToIdentifiableName(); + } + + public static class Users + { + public const string DefaultPassword = "password123"; + } + +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Common/TestEntityFactory.cs b/tests/redmine-net-api.Integration.Tests/Tests/Common/TestEntityFactory.cs new file mode 100644 index 00000000..0ff28752 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Common/TestEntityFactory.cs @@ -0,0 +1,162 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; + +public static class TestEntityFactory +{ + public static Issue CreateRandomIssuePayload( + int projectId = TestConstants.Projects.DefaultProjectId, + int trackerId = 1, + int priorityId = 2, + int statusId = 1, + string subject = null, + List customFields = null, + List watchers = null, + List uploads = null) + => new() + { + Project = projectId.ToIdentifier(), + Subject = subject ?? RandomHelper.GenerateText(9), + Description = RandomHelper.GenerateText(18), + Tracker = trackerId.ToIdentifier(), + Status = statusId.ToIssueStatusIdentifier(), + Priority = priorityId.ToIdentifier(), + CustomFields = customFields, + Watchers = watchers, + Uploads = uploads + }; + + public static User CreateRandomUserPayload(UserStatus status = UserStatus.StatusActive, int? authenticationModeId = null, + EmailNotificationType emailNotificationType = null) + { + var user = new Redmine.Net.Api.Types.User + { + Login = RandomHelper.GenerateText(12), + FirstName = RandomHelper.GenerateText(8), + LastName = RandomHelper.GenerateText(10), + Email = RandomHelper.GenerateEmail(), + Password = TestConstants.Users.DefaultPassword, + AuthenticationModeId = authenticationModeId, + MailNotification = emailNotificationType?.Name, + MustChangePassword = false, + Status = status, + }; + + return user; + } + + public static Group CreateRandomGroupPayload(string name = null, List userIds = null) + { + var group = new Redmine.Net.Api.Types.Group(name ?? RandomHelper.GenerateText(9)); + if (userIds == null || userIds.Count == 0) + { + return group; + } + foreach (var userId in userIds) + { + group.Users = [IdentifiableName.Create(userId)]; + } + return group; + } + + public static Group CreateRandomGroupPayload(string name = null, List userGroups = null) + { + var group = new Redmine.Net.Api.Types.Group(name ?? RandomHelper.GenerateText(9)); + if (userGroups == null || userGroups.Count == 0) + { + return group; + } + + group.Users = userGroups; + return group; + } + + public static (string pageName, WikiPage wikiPage) CreateRandomWikiPagePayload(string pageName = null, int version = 0, List uploads = null) + { + pageName = (pageName ?? RandomHelper.GenerateText(8)); + if (char.IsLower(pageName[0])) + { + pageName = char.ToUpper(pageName[0]) + pageName[1..]; + } + var wikiPage = new WikiPage + { + Text = RandomHelper.GenerateText(10), + Comments = RandomHelper.GenerateText(15), + Version = version, + Uploads = uploads, + }; + + return (pageName, wikiPage); + } + + public static Redmine.Net.Api.Types.Version CreateRandomVersionPayload(string name = null, + VersionStatus status = VersionStatus.Open, + VersionSharing sharing = VersionSharing.None, + int dueDateDays = 30, + string wikiPageName = null, + float? estimatedHours = null, + float? spentHours = null) + { + var version = new Redmine.Net.Api.Types.Version + { + Name = name ?? RandomHelper.GenerateText(10), + Description = RandomHelper.GenerateText(15), + Status = status, + Sharing = sharing, + DueDate = DateTime.Now.Date.AddDays(dueDateDays), + EstimatedHours = estimatedHours, + SpentHours = spentHours, + WikiPageTitle = wikiPageName, + }; + + return version; + } + + public static Redmine.Net.Api.Types.News CreateRandomNewsPayload(string title = null, List uploads = null) + { + return new Redmine.Net.Api.Types.News() + { + Title = title ?? RandomHelper.GenerateText(5), + Summary = RandomHelper.GenerateText(10), + Description = RandomHelper.GenerateText(20), + Uploads = uploads + }; + } + + public static IssueCustomField CreateRandomIssueCustomFieldWithMultipleValuesPayload() + { + return IssueCustomField.CreateMultiple(1, RandomHelper.GenerateText(8), + [RandomHelper.GenerateText(4), RandomHelper.GenerateText(4)]); + } + + public static IssueCustomField CreateRandomIssueCustomFieldWithSingleValuePayload() + { + return IssueCustomField.CreateSingle(1, RandomHelper.GenerateText(8), RandomHelper.GenerateText(4)); + } + + public static IssueRelation CreateRandomIssueRelationPayload(int issueId, int issueToId, IssueRelationType issueRelationType = IssueRelationType.Relates) + { + return new IssueRelation { IssueId = issueId, IssueToId = issueToId, Type = issueRelationType };; + } + + public static Redmine.Net.Api.Types.TimeEntry CreateRandomTimeEntryPayload(int projectId, int issueId, DateTime? spentOn = null, decimal hours = 1.5m, int? activityId = null) + { + var timeEntry = new Redmine.Net.Api.Types.TimeEntry + { + Project = projectId.ToIdentifier(), + Issue = issueId.ToIdentifier(), + SpentOn = spentOn ?? DateTime.Now.Date, + Hours = hours, + Comments = RandomHelper.GenerateText(10), + }; + + if (activityId != null) + { + timeEntry.Activity = activityId.Value.ToIdentifier(); + } + + return timeEntry; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Attachment/AttachmentTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Attachment/AttachmentTests.cs new file mode 100644 index 00000000..567e586e --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Attachment/AttachmentTests.cs @@ -0,0 +1,39 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; +using Redmine.Net.Api; +using Redmine.Net.Api.Http; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Attachment; + +[Collection(Constants.RedmineTestContainerCollection)] +public class AttachmentTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void Attachment_UploadToIssue_Should_Succeed() + { + // Arrange + var upload = FileTestHelper.UploadRandom500KbFile(fixture.RedmineManager); + Assert.NotNull(upload); + Assert.NotEmpty(upload.Token); + + var (issue, _) = IssueTestHelper.CreateRandomIssue(fixture.RedmineManager,uploads: [upload]); + Assert.NotNull(issue); + + // Act + var retrievedIssue = fixture.RedmineManager.Get( + issue.Id.ToString(), + RequestOptions.Include(RedmineKeys.ATTACHMENTS)); + + var attachment = retrievedIssue.Attachments.FirstOrDefault(); + Assert.NotNull(attachment); + + var downloadedAttachment = fixture.RedmineManager.Get(attachment.Id.ToString()); + + // Assert + Assert.NotNull(downloadedAttachment); + Assert.Equal(attachment.Id, downloadedAttachment.Id); + Assert.Equal(attachment.FileName, downloadedAttachment.FileName); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Attachment/AttachmentTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Attachment/AttachmentTestsAsync.cs new file mode 100644 index 00000000..d9335df4 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Attachment/AttachmentTestsAsync.cs @@ -0,0 +1,75 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; +using Redmine.Net.Api; +using Redmine.Net.Api.Http; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Attachment; + +[Collection(Constants.RedmineTestContainerCollection)] +public class AttachmentTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task Attachment_GetIssueWithAttachments_Should_Succeed() + { + // Arrange + var upload = await FileTestHelper.UploadRandom500KbFileAsync(fixture.RedmineManager); + var (issue, _) = await IssueTestHelper.CreateRandomIssueAsync(fixture.RedmineManager, uploads: [upload]); + + // Act + var retrievedIssue = await fixture.RedmineManager.GetAsync( + issue.Id.ToString(), + RequestOptions.Include(RedmineKeys.ATTACHMENTS)); + + // Assert + Assert.NotNull(retrievedIssue); + Assert.NotNull(retrievedIssue.Attachments); + Assert.NotEmpty(retrievedIssue.Attachments); + } + + [Fact] + public async Task Attachment_GetByIssueId_Should_Succeed() + { + // Arrange + var upload = await FileTestHelper.UploadRandom500KbFileAsync(fixture.RedmineManager); + var (issue, _) = await IssueTestHelper.CreateRandomIssueAsync(fixture.RedmineManager, uploads: [upload]); + + var retrievedIssue = await fixture.RedmineManager.GetAsync( + issue.Id.ToString(), + RequestOptions.Include(RedmineKeys.ATTACHMENTS)); + + var attachment = retrievedIssue.Attachments.FirstOrDefault(); + Assert.NotNull(attachment); + + // Act + var downloadedAttachment = await fixture.RedmineManager.GetAsync(attachment.Id.ToString()); + + // Assert + Assert.NotNull(downloadedAttachment); + Assert.Equal(attachment.Id, downloadedAttachment.Id); + Assert.Equal(attachment.FileName, downloadedAttachment.FileName); + } + + [Fact] + public async Task Attachment_Upload_MultipleFiles_Should_Succeed() + { + // Arrange & Act + var upload1 = await FileTestHelper.UploadRandom500KbFileAsync(fixture.RedmineManager); + Assert.NotNull(upload1); + Assert.NotEmpty(upload1.Token); + + var upload2 = await FileTestHelper.UploadRandom500KbFileAsync(fixture.RedmineManager); + Assert.NotNull(upload2); + Assert.NotEmpty(upload2.Token); + + // Assert + var (issue, _) = await IssueTestHelper.CreateRandomIssueAsync(fixture.RedmineManager, uploads: [upload1, upload2]); + + var retrievedIssue = await fixture.RedmineManager.GetAsync( + issue.Id.ToString(), + RequestOptions.Include(RedmineKeys.ATTACHMENTS)); + + Assert.Equal(2, retrievedIssue.Attachments.Count); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/CustomField/CustomFieldTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/CustomField/CustomFieldTests.cs new file mode 100644 index 00000000..9f64cdd4 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/CustomField/CustomFieldTests.cs @@ -0,0 +1,18 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.CustomField; + +[Collection(Constants.RedmineTestContainerCollection)] +public class CustomFieldTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void GetAllCustomFields_Should_Return_Null() + { + // Act + var customFields = fixture.RedmineManager.Get(); + + // Assert + Assert.Null(customFields); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/CustomField/CustomFieldTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/CustomField/CustomFieldTestsAsync.cs new file mode 100644 index 00000000..684882b7 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/CustomField/CustomFieldTestsAsync.cs @@ -0,0 +1,18 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.CustomField; + +[Collection(Constants.RedmineTestContainerCollection)] +public class CustomFieldTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task GetAllCustomFields_Should_Return_Null() + { + // Act + var customFields = await fixture.RedmineManager.GetAsync(); + + // Assert + Assert.Null(customFields); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Enumeration/EnumerationTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Enumeration/EnumerationTests.cs new file mode 100644 index 00000000..5436dbce --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Enumeration/EnumerationTests.cs @@ -0,0 +1,18 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Enumeration; + +[Collection(Constants.RedmineTestContainerCollection)] +public class EnumerationTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void GetDocumentCategories_Should_Succeed() => Assert.NotNull(fixture.RedmineManager.Get()); + + [Fact] + public void GetIssuePriorities_Should_Succeed() => Assert.NotNull(fixture.RedmineManager.Get()); + + [Fact] + public void GetTimeEntryActivities_Should_Succeed() => Assert.NotNull(fixture.RedmineManager.Get()); +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Enumeration/EnumerationTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Enumeration/EnumerationTestsAsync.cs new file mode 100644 index 00000000..4731d5a0 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Enumeration/EnumerationTestsAsync.cs @@ -0,0 +1,39 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Enumeration; + +[Collection(Constants.RedmineTestContainerCollection)] +public class EnumerationTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task GetDocumentCategories_Should_Succeed() + { + // Act + var categories = await fixture.RedmineManager.GetAsync(); + + // Assert + Assert.NotNull(categories); + } + + [Fact] + public async Task GetIssuePriorities_Should_Succeed() + { + // Act + var issuePriorities = await fixture.RedmineManager.GetAsync(); + + // Assert + Assert.NotNull(issuePriorities); + } + + [Fact] + public async Task GetTimeEntryActivities_Should_Succeed() + { + // Act + var activities = await fixture.RedmineManager.GetAsync(); + + // Assert + Assert.NotNull(activities); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/File/FileTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/File/FileTests.cs new file mode 100644 index 00000000..00bba896 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/File/FileTests.cs @@ -0,0 +1,101 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; +using Redmine.Net.Api.Extensions; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.File; + +[Collection(Constants.RedmineTestContainerCollection)] +public class FileTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void CreateFile_Should_Succeed() + { + var (_, token) = UploadFile(); + + var filePayload = new Redmine.Net.Api.Types.File { Token = token }; + + var createdFile = fixture.RedmineManager.Create(filePayload, TestConstants.Projects.DefaultProjectIdentifier); + Assert.Null(createdFile); // the API returns null on success when no extra fields were provided + + var files = fixture.RedmineManager.GetProjectFiles(TestConstants.Projects.DefaultProjectIdentifier); + + // Assert + Assert.NotNull(files); + Assert.NotEmpty(files.Items); + } + + [Fact] + public void CreateFile_Without_Token_Should_Fail() + { + Assert.ThrowsAny(() => + fixture.RedmineManager.Create(new Redmine.Net.Api.Types.File { Filename = "project_file.zip" }, TestConstants.Projects.DefaultProjectIdentifier)); + } + + [Fact] + public void CreateFile_With_OptionalParameters_Should_Succeed() + { + var (fileName, token) = UploadFile(); + + var filePayload = new Redmine.Net.Api.Types.File + { + Token = token, + Filename = fileName, + Description = RandomHelper.GenerateText(9), + ContentType = "text/plain", + }; + + _ = fixture.RedmineManager.Create(filePayload, TestConstants.Projects.DefaultProjectIdentifier); + + var files = fixture.RedmineManager.GetProjectFiles(TestConstants.Projects.DefaultProjectIdentifier); + + var file = files.Items.FirstOrDefault(x => x.Filename == fileName); + + Assert.NotNull(file); + Assert.True(file.Id > 0); + Assert.NotEmpty(file.Digest); + Assert.Equal(filePayload.Description, file.Description); + Assert.Equal(filePayload.ContentType, file.ContentType); + Assert.EndsWith($"/attachments/download/{file.Id}/{fileName}", file.ContentUrl); + Assert.Equal(filePayload.Version, file.Version); + } + + [Fact] + public void CreateFile_With_Version_Should_Succeed() + { + var (fileName, token) = UploadFile(); + + var filePayload = new Redmine.Net.Api.Types.File + { + Token = token, + Filename = fileName, + Description = RandomHelper.GenerateText(9), + ContentType = "text/plain", + }; + + _ = fixture.RedmineManager.Create(filePayload, TestConstants.Projects.DefaultProjectIdentifier); + + var files = fixture.RedmineManager.GetProjectFiles(TestConstants.Projects.DefaultProjectIdentifier); + var file = files.Items.FirstOrDefault(x => x.Filename == fileName); + + Assert.NotNull(file); + Assert.True(file.Id > 0); + Assert.NotEmpty(file.Digest); + Assert.Equal(filePayload.Description, file.Description); + Assert.Equal(filePayload.ContentType, file.ContentType); + Assert.EndsWith($"/attachments/download/{file.Id}/{fileName}", file.ContentUrl); + } + + private (string fileName, string token) UploadFile() + { + var bytes = "Hello World!"u8.ToArray(); + var fileName = $"{RandomHelper.GenerateText(5)}.txt"; + var upload = fixture.RedmineManager.UploadFile(bytes, fileName); + + Assert.NotNull(upload); + Assert.NotNull(upload.Token); + + return (fileName, upload.Token); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/File/FileTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/File/FileTestsAsync.cs new file mode 100644 index 00000000..2d38d3d9 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/File/FileTestsAsync.cs @@ -0,0 +1,130 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; +using Redmine.Net.Api; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Http.Extensions; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.File; + +[Collection(Constants.RedmineTestContainerCollection)] +public class FileTestsAsync(RedmineTestContainerFixture fixture) +{ + private const string PROJECT_ID = TestConstants.Projects.DefaultProjectIdentifier; + + [Fact] + public async Task CreateFile_Should_Succeed() + { + var (_, token) = await UploadFileAsync(); + + var filePayload = new Redmine.Net.Api.Types.File + { + Token = token, + }; + + var createdFile = await fixture.RedmineManager.CreateAsync(filePayload, PROJECT_ID); + Assert.Null(createdFile); + + var files = await fixture.RedmineManager.GetProjectFilesAsync(PROJECT_ID, + new RequestOptions(){ QueryString = RedmineKeys.LIMIT.WithInt(1)}); + + //Assert + Assert.NotNull(files); + Assert.NotEmpty(files.Items); + } + + [Fact] + public async Task CreateFile_Without_Token_Should_Fail() + { + await Assert.ThrowsAsync(() => fixture.RedmineManager.CreateAsync( + new Redmine.Net.Api.Types.File { Filename = "VBpMc.txt" }, PROJECT_ID)); + } + + [Fact] + public async Task CreateFile_With_OptionalParameters_Should_Succeed() + { + // Arrange + var (fileName, token) = await UploadFileAsync(); + + var filePayload = new Redmine.Net.Api.Types.File + { + Token = token, + Filename = fileName, + Description = RandomHelper.GenerateText(9), + ContentType = "text/plain", + }; + + // Act + _ = await fixture.RedmineManager.CreateAsync(filePayload, PROJECT_ID); + var files = await fixture.RedmineManager.GetProjectFilesAsync(PROJECT_ID); + var file = files.Items.FirstOrDefault(x => x.Filename == fileName); + + Assert.NotNull(file); + Assert.True(file.Id > 0); + Assert.NotEmpty(file.Digest); + Assert.Equal(filePayload.Description, file.Description); + Assert.Equal(filePayload.ContentType, file.ContentType); + Assert.EndsWith($"/attachments/download/{file.Id}/{fileName}", file.ContentUrl); + Assert.Equal(filePayload.Version, file.Version); + } + + [Fact] + public async Task CreateFile_With_Version_Should_Succeed() + { + // Arrange + var versionPayload = TestEntityFactory.CreateRandomVersionPayload(); + var version = await fixture.RedmineManager.CreateAsync(versionPayload, TestConstants.Projects.DefaultProjectIdentifier); + Assert.NotNull(version); + + var (fileName, token) = await UploadFileAsync(); + + var filePayload = new Redmine.Net.Api.Types.File + { + Token = token, + Filename = fileName, + Description = RandomHelper.GenerateText(9), + ContentType = "text/plain", + Version = version + }; + + // Act + _ = await fixture.RedmineManager.CreateAsync(filePayload, PROJECT_ID); + var files = await fixture.RedmineManager.GetProjectFilesAsync(PROJECT_ID); + var file = files.Items.FirstOrDefault(x => x.Filename == fileName); + + // Assert + Assert.NotNull(file); + Assert.True(file.Id > 0); + Assert.NotEmpty(file.Digest); + Assert.Equal(filePayload.Description, file.Description); + Assert.Equal(filePayload.ContentType, file.ContentType); + Assert.EndsWith($"/attachments/download/{file.Id}/{fileName}", file.ContentUrl); + Assert.Equal(filePayload.Version.Id, file.Version.Id); + } + + [Fact] + public async Task File_UploadLargeFile_Should_Succeed() + { + // Arrange & Act + var upload = await FileTestHelper.UploadRandom1MbFileAsync(fixture.RedmineManager); + + // Assert + Assert.NotNull(upload); + Assert.NotEmpty(upload.Token); + } + + private async Task<(string,string)> UploadFileAsync() + { + var bytes = "Hello World!"u8.ToArray(); + var fileName = $"{RandomHelper.GenerateText(5)}.txt"; + var upload = await fixture.RedmineManager.UploadFileAsync(bytes, fileName); + + Assert.NotNull(upload); + Assert.NotNull(upload.Token); + + return (fileName, upload.Token); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Group/GroupTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Group/GroupTests.cs new file mode 100644 index 00000000..6354553e --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Group/GroupTests.cs @@ -0,0 +1,110 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; +using Redmine.Net.Api; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Group; + +[Collection(Constants.RedmineTestContainerCollection)] +public class GroupTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void GetAllGroups_Should_Succeed() + { + var groups = fixture.RedmineManager.Get(); + + Assert.NotNull(groups); + } + + [Fact] + public void CreateGroup_Should_Succeed() + { + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: null); + var group = fixture.RedmineManager.Create(groupPayload); + Assert.NotNull(group); + + Assert.NotNull(group); + Assert.True(group.Id > 0); + Assert.Equal(group.Name, group.Name); + } + + [Fact] + public void GetGroup_Should_Succeed() + { + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: null); + var group = fixture.RedmineManager.Create(groupPayload); + Assert.NotNull(group); + + var retrievedGroup = fixture.RedmineManager.Get(group.Id.ToInvariantString()); + + Assert.NotNull(retrievedGroup); + Assert.Equal(group.Id, retrievedGroup.Id); + Assert.Equal(group.Name, retrievedGroup.Name); + } + + [Fact] + public void UpdateGroup_Should_Succeed() + { + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: null); + var group = fixture.RedmineManager.Create(groupPayload); + Assert.NotNull(group); + + group.Name = RandomHelper.GenerateText(7); + + fixture.RedmineManager.Update(group.Id.ToInvariantString(), group); + var retrievedGroup = fixture.RedmineManager.Get(group.Id.ToInvariantString()); + + Assert.NotNull(retrievedGroup); + Assert.Equal(group.Id, retrievedGroup.Id); + Assert.Equal(group.Name, retrievedGroup.Name); + } + + [Fact] + public void DeleteGroup_Should_Succeed() + { + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: null); + var group = fixture.RedmineManager.Create(groupPayload); + Assert.NotNull(group); + + var groupId = group.Id.ToInvariantString(); + + fixture.RedmineManager.Delete(groupId); + + Assert.Throws(() => + fixture.RedmineManager.Get(groupId)); + } + + [Fact] + public void AddUserToGroup_Should_Succeed() + { + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: null); + var group = fixture.RedmineManager.Create(groupPayload); + Assert.NotNull(group); + + fixture.RedmineManager.AddUserToGroup(group.Id, userId: 1); + var updatedGroup = fixture.RedmineManager.Get(group.Id.ToString(), RequestOptions.Include(RedmineKeys.USERS)); + + Assert.NotNull(updatedGroup); + Assert.NotNull(updatedGroup.Users); + Assert.Contains(updatedGroup.Users, u => u.Id == 1); + } + + [Fact] + public void RemoveUserFromGroup_Should_Succeed() + { + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: null); + var group = fixture.RedmineManager.Create(groupPayload); + Assert.NotNull(group); + + fixture.RedmineManager.AddUserToGroup(group.Id, userId: 1); + + fixture.RedmineManager.RemoveUserFromGroup(group.Id, userId: 1); + var updatedGroup = fixture.RedmineManager.Get(group.Id.ToString(), RequestOptions.Include(RedmineKeys.USERS)); + + Assert.NotNull(updatedGroup); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Group/GroupTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Group/GroupTestsAsync.cs new file mode 100644 index 00000000..3471cd4d --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Group/GroupTestsAsync.cs @@ -0,0 +1,130 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; +using Redmine.Net.Api; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Group; + +[Collection(Constants.RedmineTestContainerCollection)] +public class GroupTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task GetAllGroups_Should_Succeed() + { + // Act + var groups = await fixture.RedmineManager.GetAsync(); + + // Assert + Assert.NotNull(groups); + } + + [Fact] + public async Task CreateGroup_Should_Succeed() + { + // Arrange + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: null); + + // Act + var group = await fixture.RedmineManager.CreateAsync(groupPayload); + + // Assert + Assert.NotNull(group); + Assert.True(group.Id > 0); + Assert.Equal(groupPayload.Name, group.Name); + } + + [Fact] + public async Task GetGroup_Should_Succeed() + { + // Arrange + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: null); + var group = await fixture.RedmineManager.CreateAsync(groupPayload); + Assert.NotNull(group); + + // Act + var retrievedGroup = await fixture.RedmineManager.GetAsync(group.Id.ToInvariantString()); + + // Assert + Assert.NotNull(retrievedGroup); + Assert.Equal(group.Id, retrievedGroup.Id); + Assert.Equal(group.Name, retrievedGroup.Name); + } + + [Fact] + public async Task UpdateGroup_Should_Succeed() + { + // Arrange + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: null); + var group = await fixture.RedmineManager.CreateAsync(groupPayload); + Assert.NotNull(group); + + group.Name = RandomHelper.GenerateText(7); + + // Act + await fixture.RedmineManager.UpdateAsync(group.Id.ToInvariantString(), group); + var retrievedGroup = await fixture.RedmineManager.GetAsync(group.Id.ToInvariantString()); + + // Assert + Assert.NotNull(retrievedGroup); + Assert.Equal(group.Id, retrievedGroup.Id); + Assert.Equal(group.Name, retrievedGroup.Name); + } + + [Fact] + public async Task DeleteGroup_Should_Succeed() + { + // Arrange + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: null); + var group = await fixture.RedmineManager.CreateAsync(groupPayload); + Assert.NotNull(group); + + var groupId = group.Id.ToInvariantString(); + + // Act + await fixture.RedmineManager.DeleteAsync(groupId); + + // Assert + await Assert.ThrowsAsync(async () => + await fixture.RedmineManager.GetAsync(groupId)); + } + + [Fact] + public async Task AddUserToGroup_Should_Succeed() + { + // Arrange + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: null); + var group = await fixture.RedmineManager.CreateAsync(groupPayload); + Assert.NotNull(group); + + // Act + await fixture.RedmineManager.AddUserToGroupAsync(group.Id, userId: 1); + var updatedGroup = await fixture.RedmineManager.GetAsync(group.Id.ToString(), RequestOptions.Include(RedmineKeys.USERS)); + + // Assert + Assert.NotNull(updatedGroup); + Assert.NotNull(updatedGroup.Users); + Assert.Contains(updatedGroup.Users, ug => ug.Id == 1); + } + + [Fact] + public async Task RemoveUserFromGroup_Should_Succeed() + { + // Arrange + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: null); + var group = await fixture.RedmineManager.CreateAsync(groupPayload); + Assert.NotNull(group); + + await fixture.RedmineManager.AddUserToGroupAsync(group.Id, userId: 1); + + // Act + await fixture.RedmineManager.RemoveUserFromGroupAsync(group.Id, userId: 1); + var updatedGroup = await fixture.RedmineManager.GetAsync(group.Id.ToString(), RequestOptions.Include(RedmineKeys.USERS)); + + // Assert + Assert.NotNull(updatedGroup); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueAttachmentUploadTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueAttachmentUploadTests.cs new file mode 100644 index 00000000..e74b62da --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueAttachmentUploadTests.cs @@ -0,0 +1,43 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; +using Redmine.Net.Api; +using Redmine.Net.Api.Http; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueAttachmentTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void UploadAttachmentAndAttachToIssue_Should_Succeed() + { + // Arrange + var (issue, _) = IssueTestHelper.CreateRandomIssue(fixture.RedmineManager); + + var content = "Test attachment content"u8.ToArray(); + var fileName = "test_attachment.txt"; + var upload = fixture.RedmineManager.UploadFile(content, fileName); + Assert.NotNull(upload); + Assert.NotEmpty(upload.Token); + + // Act + var updateIssue = new Redmine.Net.Api.Types.Issue + { + Subject = $"Test issue for attachment {RandomHelper.GenerateText(5)}", + Uploads = [upload] + }; + fixture.RedmineManager.Update(issue.Id.ToString(), updateIssue); + + + var retrievedIssue = fixture.RedmineManager.Get( + issue.Id.ToString(), + RequestOptions.Include(RedmineKeys.ATTACHMENTS)); + + // Assert + Assert.NotNull(retrievedIssue); + Assert.NotEmpty(retrievedIssue.Attachments); + Assert.Contains(retrievedIssue.Attachments, a => a.FileName == fileName); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueAttachmentUploadTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueAttachmentUploadTestsAsync.cs new file mode 100644 index 00000000..342400a0 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueAttachmentUploadTestsAsync.cs @@ -0,0 +1,44 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; +using Redmine.Net.Api; +using Redmine.Net.Api.Http; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueAttachmentTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task UploadAttachmentAndAttachToIssue_Should_Succeed() + { + // Arrange + var (issue, _) = await IssueTestHelper.CreateRandomIssueAsync(fixture.RedmineManager); + + var fileContent = "Test attachment content"u8.ToArray(); + var filename = "test_attachment.txt"; + var upload = await fixture.RedmineManager.UploadFileAsync(fileContent, filename); + Assert.NotNull(upload); + Assert.NotEmpty(upload.Token); + + // Prepare issue with attachment + var updateIssue = new Redmine.Net.Api.Types.Issue + { + Subject = $"Test issue for attachment {RandomHelper.GenerateText(5)}", + Uploads = [upload] + }; + + // Act + await fixture.RedmineManager.UpdateAsync(issue.Id.ToString(), updateIssue); + + var retrievedIssue = + await fixture.RedmineManager.GetAsync(issue.Id.ToString(), RequestOptions.Include(RedmineKeys.ATTACHMENTS)); + + // Assert + Assert.NotNull(retrievedIssue); + Assert.NotNull(retrievedIssue.Attachments); + Assert.NotEmpty(retrievedIssue.Attachments); + Assert.Contains(retrievedIssue.Attachments, a => a.FileName == filename); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueTests.cs new file mode 100644 index 00000000..27e3033e --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueTests.cs @@ -0,0 +1,133 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void CreateIssue_Should_Succeed() + { + //Arrange + var (issue, _) = IssueTestHelper.CreateRandomIssue(fixture.RedmineManager); + + // Assert + Assert.NotNull(issue); + Assert.True(issue.Id > 0); + } + + [Fact] + public void CreateIssue_With_IssueCustomField_Should_Succeed() + { + //Arrange + var (issue, _) = IssueTestHelper.CreateRandomIssue(fixture.RedmineManager, customFields: + [ + TestEntityFactory.CreateRandomIssueCustomFieldWithSingleValuePayload() + ]); + + // Assert + Assert.NotNull(issue); + Assert.True(issue.Id > 0); + } + + [Fact] + public void GetIssue_Should_Succeed() + { + //Arrange + var (issue, issuePayload) = IssueTestHelper.CreateRandomIssue(fixture.RedmineManager); + + Assert.NotNull(issue); + Assert.True(issue.Id > 0); + + var issueId = issue.Id.ToInvariantString(); + + //Act + var retrievedIssue = fixture.RedmineManager.Get(issueId); + + //Assert + IssueTestHelper.AssertBasic(issuePayload, retrievedIssue); + } + + [Fact] + public void UpdateIssue_Should_Succeed() + { + //Arrange + var (issue, _) = IssueTestHelper.CreateRandomIssue(fixture.RedmineManager); + + issue.Subject = RandomHelper.GenerateText(9); + issue.Description = RandomHelper.GenerateText(18); + issue.Status = 2.ToIssueStatusIdentifier(); + issue.Notes = RandomHelper.GenerateText("Note"); + + var issueId = issue.Id.ToInvariantString(); + + //Act + fixture.RedmineManager.Update(issueId, issue); + var updatedIssue = fixture.RedmineManager.Get(issueId); + + //Assert + IssueTestHelper.AssertBasic(issue, updatedIssue); + Assert.Equal(issue.Subject, updatedIssue.Subject); + Assert.Equal(issue.Description, updatedIssue.Description); + Assert.Equal(issue.Status.Id, updatedIssue.Status.Id); + } + + [Fact] + public void DeleteIssue_Should_Succeed() + { + //Arrange + var (issue, _) = IssueTestHelper.CreateRandomIssue(fixture.RedmineManager); + + var issueId = issue.Id.ToInvariantString(); + + //Act + fixture.RedmineManager.Delete(issueId); + + //Assert + Assert.Throws(() => fixture.RedmineManager.Get(issueId)); + } + + [Fact] + public void GetIssue_With_Watchers_And_Relations_Should_Succeed() + { + var userPayload = TestEntityFactory.CreateRandomUserPayload(); + var createdUser = fixture.RedmineManager.Create(userPayload); + Assert.NotNull(createdUser); + + var userId = createdUser.Id; + + var (firstIssue, _) = IssueTestHelper.CreateRandomIssue(fixture.RedmineManager, customFields: + [ + IssueCustomField.CreateMultiple(1, RandomHelper.GenerateText(8), + [RandomHelper.GenerateText(4), RandomHelper.GenerateText(4)]) + ], watchers: + [new Watcher() { Id = 1 }, new Watcher() { Id = userId }]); + + var (secondIssue, _) = IssueTestHelper.CreateRandomIssue(fixture.RedmineManager, + customFields: [TestEntityFactory.CreateRandomIssueCustomFieldWithMultipleValuesPayload()], + watchers: [new Watcher() { Id = 1 }, new Watcher() { Id = userId }]); + + var issueRelation = new Redmine.Net.Api.Types.IssueRelation() + { + Type = IssueRelationType.Relates, + IssueToId = firstIssue.Id, + }; + _ = fixture.RedmineManager.Create(issueRelation, secondIssue.Id.ToInvariantString()); + + //Act + var retrievedIssue = fixture.RedmineManager.Get(secondIssue.Id.ToInvariantString(), + RequestOptions.Include($"{Include.Issue.Watchers},{Include.Issue.Relations}")); + + //Assert + IssueTestHelper.AssertBasic(secondIssue, retrievedIssue); + Assert.NotNull(retrievedIssue.Watchers); + Assert.NotNull(retrievedIssue.Relations); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueTestsAsync.cs new file mode 100644 index 00000000..9118390c --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueTestsAsync.cs @@ -0,0 +1,157 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueTestsAsync(RedmineTestContainerFixture fixture) +{ + private static readonly IdentifiableName ProjectIdName = IdentifiableName.Create(1); + + private async Task CreateTestIssueAsync(List customFields = null, + List watchers = null) + { + var issue = new Redmine.Net.Api.Types.Issue + { + Project = ProjectIdName, + Subject = RandomHelper.GenerateText(9), + Description = RandomHelper.GenerateText(18), + Tracker = 1.ToIdentifier(), + Status = 1.ToIssueStatusIdentifier(), + Priority = 2.ToIdentifier(), + CustomFields = customFields, + Watchers = watchers + }; + return await fixture.RedmineManager.CreateAsync(issue); + } + + [Fact] + public async Task CreateIssue_Should_Succeed() + { + //Arrange + var issueData = new Redmine.Net.Api.Types.Issue + { + Project = ProjectIdName, + Subject = RandomHelper.GenerateText(9), + Description = RandomHelper.GenerateText(18), + Tracker = 2.ToIdentifier(), + Status = 1.ToIssueStatusIdentifier(), + Priority = 3.ToIdentifier(), + StartDate = DateTime.Now.Date, + DueDate = DateTime.Now.Date.AddDays(7), + EstimatedHours = 8, + CustomFields = + [ + IssueCustomField.CreateSingle(1, RandomHelper.GenerateText(8), RandomHelper.GenerateText(4)) + ] + }; + + //Act + var cr = await fixture.RedmineManager.CreateAsync(issueData); + var createdIssue = await fixture.RedmineManager.GetAsync(cr.Id.ToString()); + + //Assert + Assert.NotNull(createdIssue); + Assert.True(createdIssue.Id > 0); + Assert.Equal(issueData.Subject, createdIssue.Subject); + Assert.Equal(issueData.Description, createdIssue.Description); + Assert.Equal(issueData.Project.Id, createdIssue.Project.Id); + Assert.Equal(issueData.Tracker.Id, createdIssue.Tracker.Id); + Assert.Equal(issueData.Status.Id, createdIssue.Status.Id); + Assert.Equal(issueData.Priority.Id, createdIssue.Priority.Id); + Assert.Equal(issueData.StartDate, createdIssue.StartDate); + Assert.Equal(issueData.DueDate, createdIssue.DueDate); + // Assert.Equal(issueData.EstimatedHours, createdIssue.EstimatedHours); + } + + [Fact] + public async Task GetIssue_Should_Succeed() + { + //Arrange + var createdIssue = await CreateTestIssueAsync(); + Assert.NotNull(createdIssue); + + var issueId = createdIssue.Id.ToInvariantString(); + + //Act + var retrievedIssue = await fixture.RedmineManager.GetAsync(issueId); + + //Assert + Assert.NotNull(retrievedIssue); + Assert.Equal(createdIssue.Id, retrievedIssue.Id); + Assert.Equal(createdIssue.Subject, retrievedIssue.Subject); + Assert.Equal(createdIssue.Description, retrievedIssue.Description); + Assert.Equal(createdIssue.Project.Id, retrievedIssue.Project.Id); + } + + [Fact] + public async Task UpdateIssue_Should_Succeed() + { + //Arrange + var createdIssue = await CreateTestIssueAsync(); + Assert.NotNull(createdIssue); + + var updatedSubject = RandomHelper.GenerateText(9); + var updatedDescription = RandomHelper.GenerateText(18); + var updatedStatusId = 2; + + createdIssue.Subject = updatedSubject; + createdIssue.Description = updatedDescription; + createdIssue.Status = updatedStatusId.ToIssueStatusIdentifier(); + createdIssue.Notes = RandomHelper.GenerateText("Note"); + + var issueId = createdIssue.Id.ToInvariantString(); + + //Act + await fixture.RedmineManager.UpdateAsync(issueId, createdIssue); + var retrievedIssue = await fixture.RedmineManager.GetAsync(issueId); + + //Assert + Assert.NotNull(retrievedIssue); + Assert.Equal(createdIssue.Id, retrievedIssue.Id); + Assert.Equal(updatedSubject, retrievedIssue.Subject); + Assert.Equal(updatedDescription, retrievedIssue.Description); + Assert.Equal(updatedStatusId, retrievedIssue.Status.Id); + } + + [Fact] + public async Task DeleteIssue_Should_Succeed() + { + //Arrange + var createdIssue = await CreateTestIssueAsync(); + Assert.NotNull(createdIssue); + + var issueId = createdIssue.Id.ToInvariantString(); + + //Act + await fixture.RedmineManager.DeleteAsync(issueId); + + //Assert + await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetAsync(issueId)); + } + + [Fact] + public async Task GetIssue_With_Watchers_And_Relations_Should_Succeed() + { + var createdIssue = await CreateTestIssueAsync( + [ + IssueCustomField.CreateMultiple(1, RandomHelper.GenerateText(8), + [RandomHelper.GenerateText(4), RandomHelper.GenerateText(4)]) + ], + [new Watcher() { Id = 1 }]); + + Assert.NotNull(createdIssue); + + //Act + var retrievedIssue = await fixture.RedmineManager.GetAsync(createdIssue.Id.ToInvariantString(), + RequestOptions.Include($"{Include.Issue.Watchers},{Include.Issue.Relations}")); + + //Assert + Assert.NotNull(retrievedIssue); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueWatcherTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueWatcherTests.cs new file mode 100644 index 00000000..fb62e53c --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueWatcherTests.cs @@ -0,0 +1,43 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; +using Redmine.Net.Api; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueWatcherTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void AddWatcher_Should_Succeed() + { + var (issue, _) = IssueTestHelper.CreateRandomIssue(fixture.RedmineManager); + const int userId = 1; + + fixture.RedmineManager.AddWatcherToIssue(issue.Id, userId); + + var updated = fixture.RedmineManager.Get( + issue.Id.ToString(), + RequestOptions.Include(RedmineKeys.WATCHERS)); + + Assert.Contains(updated.Watchers, w => w.Id == userId); + } + + [Fact] + public void RemoveWatcher_Should_Succeed() + { + var (issue, _) = IssueTestHelper.CreateRandomIssue(fixture.RedmineManager); + const int userId = 1; + + fixture.RedmineManager.AddWatcherToIssue(issue.Id, userId); + fixture.RedmineManager.RemoveWatcherFromIssue(issue.Id, userId); + + var updated = fixture.RedmineManager.Get( + issue.Id.ToString(), + RequestOptions.Include(RedmineKeys.WATCHERS)); + + Assert.DoesNotContain(updated.Watchers ?? [], w => w.Id == userId); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueWatcherTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueWatcherTestsAsync.cs new file mode 100644 index 00000000..b9d5c567 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueWatcherTestsAsync.cs @@ -0,0 +1,68 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueWatcherTestsAsync(RedmineTestContainerFixture fixture) +{ + private async Task CreateTestIssueAsync() + { + var issue = new Redmine.Net.Api.Types.Issue + { + Project = new IdentifiableName { Id = 1 }, + Tracker = new IdentifiableName { Id = 1 }, + Status = new Redmine.Net.Api.Types.IssueStatus { Id = 1 }, + Priority = new IdentifiableName { Id = 4 }, + Subject = $"Test issue subject {Guid.NewGuid()}", + Description = "Test issue description" + }; + + return await fixture.RedmineManager.CreateAsync(issue); + } + + [Fact] + public async Task AddWatcher_Should_Succeed() + { + // Arrange + var issue = await CreateTestIssueAsync(); + Assert.NotNull(issue); + + const int userId = 1; + + // Act + await fixture.RedmineManager.AddWatcherToIssueAsync(issue.Id, userId); + + var updatedIssue = await fixture.RedmineManager.GetAsync(issue.Id.ToString(), RequestOptions.Include(RedmineKeys.WATCHERS)); + + // Assert + Assert.NotNull(updatedIssue); + Assert.NotNull(updatedIssue.Watchers); + Assert.Contains(updatedIssue.Watchers, w => w.Id == userId); + } + + [Fact] + public async Task RemoveWatcher_Should_Succeed() + { + // Arrange + var issue = await CreateTestIssueAsync(); + Assert.NotNull(issue); + + const int userId = 1; + + await fixture.RedmineManager.AddWatcherToIssueAsync(issue.Id, userId); + + // Act + await fixture.RedmineManager.RemoveWatcherFromIssueAsync(issue.Id, userId); + + var updatedIssue = await fixture.RedmineManager.GetAsync(issue.Id.ToString(), RequestOptions.Include(RedmineKeys.WATCHERS)); + + // Assert + Assert.NotNull(updatedIssue); + Assert.DoesNotContain(updatedIssue.Watchers ?? [], w => w.Id == userId); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueCategory/IssueCategoryTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueCategory/IssueCategoryTests.cs new file mode 100644 index 00000000..78744ae5 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueCategory/IssueCategoryTests.cs @@ -0,0 +1,76 @@ +using System.Collections.Specialized; +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; +using Redmine.Net.Api; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.IssueCategory; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueCategoryTests(RedmineTestContainerFixture fixture) +{ + private const string PROJECT_ID = TestConstants.Projects.DefaultProjectIdentifier; + + private Redmine.Net.Api.Types.IssueCategory CreateCategory() + { + return fixture.RedmineManager.Create( + new Redmine.Net.Api.Types.IssueCategory { Name = $"Test Category {Guid.NewGuid()}" }, + PROJECT_ID); + } + + [Fact] + public void GetProjectIssueCategories_Should_Succeed() => + Assert.NotNull(fixture.RedmineManager.Get(new RequestOptions() + { + QueryString = new NameValueCollection() + { + {RedmineKeys.PROJECT_ID, PROJECT_ID} + } + })); + + [Fact] + public void CreateIssueCategory_Should_Succeed() + { + var cat = new Redmine.Net.Api.Types.IssueCategory { Name = $"Cat {Guid.NewGuid()}" }; + var created = fixture.RedmineManager.Create(cat, PROJECT_ID); + + Assert.True(created.Id > 0); + Assert.Equal(cat.Name, created.Name); + } + + [Fact] + public void GetIssueCategory_Should_Succeed() + { + var created = CreateCategory(); + var retrieved = fixture.RedmineManager.Get(created.Id.ToInvariantString()); + + Assert.Equal(created.Id, retrieved.Id); + Assert.Equal(created.Name, retrieved.Name); + } + + [Fact] + public void UpdateIssueCategory_Should_Succeed() + { + var created = CreateCategory(); + created.Name = $"Updated {Guid.NewGuid()}"; + + fixture.RedmineManager.Update(created.Id.ToInvariantString(), created); + var retrieved = fixture.RedmineManager.Get(created.Id.ToInvariantString()); + + Assert.Equal(created.Name, retrieved.Name); + } + + [Fact] + public void DeleteIssueCategory_Should_Succeed() + { + var created = CreateCategory(); + var id = created.Id.ToInvariantString(); + + fixture.RedmineManager.Delete(id); + + Assert.Throws(() => fixture.RedmineManager.Get(id)); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueCategory/IssueCategoryTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueCategory/IssueCategoryTestsAsync.cs new file mode 100644 index 00000000..1b0601c1 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueCategory/IssueCategoryTestsAsync.cs @@ -0,0 +1,111 @@ +using System.Collections.Specialized; +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; +using Redmine.Net.Api; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.IssueCategory; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueCategoryTestsAsync(RedmineTestContainerFixture fixture) +{ + private const string PROJECT_ID = TestConstants.Projects.DefaultProjectIdentifier; + + private async Task CreateRandomIssueCategoryAsync() + { + var category = new Redmine.Net.Api.Types.IssueCategory + { + Name = RandomHelper.GenerateText(5) + }; + + return await fixture.RedmineManager.CreateAsync(category, PROJECT_ID); + } + + [Fact] + public async Task GetProjectIssueCategories_Should_Succeed() + { + // Arrange + var category = await CreateRandomIssueCategoryAsync(); + Assert.NotNull(category); + + // Act + var categories = await fixture.RedmineManager.GetAsync(new RequestOptions() + { + QueryString = new NameValueCollection() + { + {RedmineKeys.PROJECT_ID, PROJECT_ID} + } + }); + + // Assert + Assert.NotNull(categories); + } + + [Fact] + public async Task CreateIssueCategory_Should_Succeed() + { + // Arrange & Act + var category = await CreateRandomIssueCategoryAsync(); + + // Assert + Assert.NotNull(category); + Assert.True(category.Id > 0); + } + + [Fact] + public async Task GetIssueCategory_Should_Succeed() + { + // Arrange + var createdCategory = await CreateRandomIssueCategoryAsync(); + Assert.NotNull(createdCategory); + + // Act + var retrievedCategory = await fixture.RedmineManager.GetAsync(createdCategory.Id.ToInvariantString()); + + // Assert + Assert.NotNull(retrievedCategory); + Assert.Equal(createdCategory.Id, retrievedCategory.Id); + Assert.Equal(createdCategory.Name, retrievedCategory.Name); + } + + [Fact] + public async Task UpdateIssueCategory_Should_Succeed() + { + // Arrange + var createdCategory = await CreateRandomIssueCategoryAsync(); + Assert.NotNull(createdCategory); + + var updatedName = $"Updated Test Category {Guid.NewGuid()}"; + createdCategory.Name = updatedName; + + // Act + await fixture.RedmineManager.UpdateAsync(createdCategory.Id.ToInvariantString(), createdCategory); + var retrievedCategory = await fixture.RedmineManager.GetAsync(createdCategory.Id.ToInvariantString()); + + // Assert + Assert.NotNull(retrievedCategory); + Assert.Equal(createdCategory.Id, retrievedCategory.Id); + Assert.Equal(updatedName, retrievedCategory.Name); + } + + [Fact] + public async Task DeleteIssueCategory_Should_Succeed() + { + // Arrange + var createdCategory = await CreateRandomIssueCategoryAsync(); + Assert.NotNull(createdCategory); + + var categoryId = createdCategory.Id.ToInvariantString(); + + // Act + await fixture.RedmineManager.DeleteAsync(categoryId); + + // Assert + await Assert.ThrowsAsync(async () => + await fixture.RedmineManager.GetAsync(categoryId)); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueRelation/IssueRelationTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueRelation/IssueRelationTests.cs new file mode 100644 index 00000000..11efdfce --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueRelation/IssueRelationTests.cs @@ -0,0 +1,35 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; +using Redmine.Net.Api; +using Redmine.Net.Api.Http; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.IssueRelation; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueRelationTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void CreateIssueRelation_Should_Succeed() + { + var (relation, i1, i2) = IssueTestHelper.CreateRandomIssueRelation(fixture.RedmineManager); + + Assert.NotNull(relation); + Assert.True(relation.Id > 0); + Assert.Equal(i1.Id, relation.IssueId); + Assert.Equal(i2.Id, relation.IssueToId); + } + + [Fact] + public void DeleteIssueRelation_Should_Succeed() + { + var (rel, _, _) = IssueTestHelper.CreateRandomIssueRelation(fixture.RedmineManager); + fixture.RedmineManager.Delete(rel.Id.ToString()); + + var issue = fixture.RedmineManager.Get( + rel.IssueId.ToString(), + RequestOptions.Include(RedmineKeys.RELATIONS)); + + Assert.Null(issue.Relations?.FirstOrDefault(r => r.Id == rel.Id)); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueRelation/IssueRelationTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueRelation/IssueRelationTestsAsync.cs new file mode 100644 index 00000000..ac1b5839 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueRelation/IssueRelationTestsAsync.cs @@ -0,0 +1,51 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; +using Redmine.Net.Api; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.IssueRelation; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueRelationTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task CreateIssueRelation_Should_Succeed() + { + // Arrange + var (issue1, issue2) = await IssueTestHelper.CreateRandomTwoIssuesAsync(fixture.RedmineManager); + + var relation = new Redmine.Net.Api.Types.IssueRelation + { + IssueId = issue1.Id, + IssueToId = issue2.Id, + Type = IssueRelationType.Relates + }; + + // Act + var createdRelation = await fixture.RedmineManager.CreateAsync(relation, issue1.Id.ToString()); + + // Assert + Assert.NotNull(createdRelation); + Assert.True(createdRelation.Id > 0); + Assert.Equal(relation.IssueId, createdRelation.IssueId); + Assert.Equal(relation.IssueToId, createdRelation.IssueToId); + Assert.Equal(relation.Type, createdRelation.Type); + } + + [Fact] + public async Task DeleteIssueRelation_Should_Succeed() + { + // Arrange + var (relation, _, _) = await IssueTestHelper.CreateRandomIssueRelationAsync(fixture.RedmineManager); + Assert.NotNull(relation); + + // Act & Assert + await fixture.RedmineManager.DeleteAsync(relation.Id.ToString()); + + var issue = await fixture.RedmineManager.GetAsync(relation.IssueId.ToString(), RequestOptions.Include(RedmineKeys.RELATIONS)); + + Assert.Null(issue.Relations?.FirstOrDefault(r => r.Id == relation.Id)); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueStatus/IssueStatusTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueStatus/IssueStatusTests.cs new file mode 100644 index 00000000..b18abf13 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueStatus/IssueStatusTests.cs @@ -0,0 +1,16 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.IssueStatus; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueStatusTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void GetAllIssueStatuses_Should_Succeed() + { + var statuses = fixture.RedmineManager.Get(); + Assert.NotNull(statuses); + Assert.NotEmpty(statuses); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueStatus/IssueStatusTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueStatus/IssueStatusTestsAsync.cs new file mode 100644 index 00000000..a1c7bd5c --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueStatus/IssueStatusTestsAsync.cs @@ -0,0 +1,19 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.IssueStatus; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueStatusAsyncTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task GetAllIssueStatuses_Should_Succeed() + { + // Act + var statuses = await fixture.RedmineManager.GetAsync(); + + // Assert + Assert.NotNull(statuses); + Assert.NotEmpty(statuses); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Journal/JournalTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Journal/JournalTests.cs new file mode 100644 index 00000000..b3923d8e --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Journal/JournalTests.cs @@ -0,0 +1,40 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; +using Redmine.Net.Api; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Journal; + +[Collection(Constants.RedmineTestContainerCollection)] +public class JournalTests(RedmineTestContainerFixture fixture) +{ + private Redmine.Net.Api.Types.Issue CreateRandomIssue() + { + var issue = TestEntityFactory.CreateRandomIssuePayload(); + return fixture.RedmineManager.Create(issue); + } + + [Fact] + public void Get_Issue_With_Journals_Should_Succeed() + { + // Arrange + var testIssue = CreateRandomIssue(); + Assert.NotNull(testIssue); + + testIssue.Notes = "This is a test note to create a journal entry."; + fixture.RedmineManager.Update(testIssue.Id.ToInvariantString(), testIssue); + + // Act + var issueWithJournals = fixture.RedmineManager.Get( + testIssue.Id.ToInvariantString(), + RequestOptions.Include(RedmineKeys.JOURNALS)); + + // Assert + Assert.NotNull(issueWithJournals); + Assert.NotNull(issueWithJournals.Journals); + Assert.True(issueWithJournals.Journals.Count > 0); + Assert.Contains(issueWithJournals.Journals, j => j.Notes == testIssue.Notes); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Journal/JournalTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Journal/JournalTestsAsync.cs new file mode 100644 index 00000000..b208a84d --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Journal/JournalTestsAsync.cs @@ -0,0 +1,42 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; +using Redmine.Net.Api; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Journal; + +[Collection(Constants.RedmineTestContainerCollection)] +public class JournalTestsAsync(RedmineTestContainerFixture fixture) +{ + private async Task CreateRandomIssueAsync() + { + var issuePayload = TestEntityFactory.CreateRandomIssuePayload(); + return await fixture.RedmineManager.CreateAsync(issuePayload); + } + + [Fact] + public async Task Get_Issue_With_Journals_Should_Succeed() + { + //Arrange + var testIssue = await CreateRandomIssueAsync(); + Assert.NotNull(testIssue); + + var issueIdToTest = testIssue.Id.ToInvariantString(); + + testIssue.Notes = "This is a test note to create a journal entry."; + await fixture.RedmineManager.UpdateAsync(issueIdToTest, testIssue); + + //Act + var issueWithJournals = await fixture.RedmineManager.GetAsync( + issueIdToTest, + RequestOptions.Include(RedmineKeys.JOURNALS)); + + //Assert + Assert.NotNull(issueWithJournals); + Assert.NotNull(issueWithJournals.Journals); + Assert.True(issueWithJournals.Journals.Count > 0, "Issue should have journal entries."); + Assert.Contains(issueWithJournals.Journals, j => j.Notes == testIssue.Notes); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/News/NewsTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/News/NewsTests.cs new file mode 100644 index 00000000..fc8f2050 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/News/NewsTests.cs @@ -0,0 +1,32 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; +using Redmine.Net.Api.Extensions; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.News; + +[Collection(Constants.RedmineTestContainerCollection)] +public class NewsTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void GetAllNews_Should_Succeed() + { + _ = fixture.RedmineManager.AddProjectNews(TestConstants.Projects.DefaultProjectIdentifier, TestEntityFactory.CreateRandomNewsPayload()); + + _ = fixture.RedmineManager.AddProjectNews(TestConstants.Projects.DefaultProjectIdentifier, TestEntityFactory.CreateRandomNewsPayload()); + + var news = fixture.RedmineManager.Get(); + + Assert.NotNull(news); + } + + [Fact] + public void GetProjectNews_Should_Succeed() + { + _ = fixture.RedmineManager.AddProjectNews(TestConstants.Projects.DefaultProjectIdentifier, TestEntityFactory.CreateRandomNewsPayload()); + + var news = fixture.RedmineManager.GetProjectNews(TestConstants.Projects.DefaultProjectIdentifier); + + Assert.NotNull(news); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/News/NewsTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/News/NewsTestsAsync.cs new file mode 100644 index 00000000..8002e556 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/News/NewsTestsAsync.cs @@ -0,0 +1,52 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; +using Redmine.Net.Api.Extensions; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.News; + +[Collection(Constants.RedmineTestContainerCollection)] +public class NewsTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task GetAllNews_Should_Succeed() + { + // Arrange + _ = await fixture.RedmineManager.AddProjectNewsAsync(TestConstants.Projects.DefaultProjectIdentifier, TestEntityFactory.CreateRandomNewsPayload()); + + _ = await fixture.RedmineManager.AddProjectNewsAsync(TestConstants.Projects.DefaultProjectIdentifier, TestEntityFactory.CreateRandomNewsPayload()); + + // Act + var news = await fixture.RedmineManager.GetAsync(); + + // Assert + Assert.NotNull(news); + } + + [Fact] + public async Task GetProjectNews_Should_Succeed() + { + // Arrange + var newsCreated = await fixture.RedmineManager.AddProjectNewsAsync(TestConstants.Projects.DefaultProjectIdentifier, TestEntityFactory.CreateRandomNewsPayload()); + + // Act + var news = await fixture.RedmineManager.GetProjectNewsAsync(TestConstants.Projects.DefaultProjectIdentifier); + + // Assert + Assert.NotNull(news); + } + + [Fact] + public async Task News_AddWithUploads_Should_Succeed() + { + // Arrange + var newsPayload = TestEntityFactory.CreateRandomNewsPayload(); + var newsCreated = await fixture.RedmineManager.AddProjectNewsAsync(TestConstants.Projects.DefaultProjectIdentifier, newsPayload); + + // Act + var news = await fixture.RedmineManager.GetProjectNewsAsync(TestConstants.Projects.DefaultProjectIdentifier); + + // Assert + Assert.NotNull(news); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Project/ProjectTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Project/ProjectTestsAsync.cs new file mode 100644 index 00000000..fd3d4137 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Project/ProjectTestsAsync.cs @@ -0,0 +1,85 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Project; + +[Collection(Constants.RedmineTestContainerCollection)] +public class ProjectTestsAsync(RedmineTestContainerFixture fixture) +{ + private async Task CreateEntityAsync(string subjectSuffix = null) + { + var entity = new Redmine.Net.Api.Types.Project + { + Identifier = RandomHelper.GenerateText(5).ToLowerInvariant(), + Name = "test-random", + }; + + return await fixture.RedmineManager.CreateAsync(entity); + } + + [Fact] + public async Task CreateProject_Should_Succeed() + { + //Arrange + var projectName = RandomHelper.GenerateText(7); + var data = new Redmine.Net.Api.Types.Project + { + Name = projectName, + Identifier = projectName.ToLowerInvariant(), + Description = RandomHelper.GenerateText(7), + HomePage = RandomHelper.GenerateText(7), + IsPublic = true, + InheritMembers = true, + + EnabledModules = [ + new ProjectEnabledModule("files"), + new ProjectEnabledModule("wiki") + ], + + Trackers = + [ + new ProjectTracker(1), + new ProjectTracker(2), + new ProjectTracker(3), + ], + + //CustomFieldValues = [IdentifiableName.Create(1, "cf1"), IdentifiableName.Create(2, "cf2")] + // IssueCustomFields = + // [ + // IssueCustomField.CreateSingle(1, RandomHelper.GenerateText(5), RandomHelper.GenerateText(7)) + // ] + }; + + //Act + var createdProject = await fixture.RedmineManager.CreateAsync(data); + Assert.NotNull(createdProject); + } + + [Fact] + public async Task DeleteIssue_Should_Succeed() + { + //Arrange + var createdEntity = await CreateEntityAsync("DeleteTest"); + Assert.NotNull(createdEntity); + + var id = createdEntity.Id.ToInvariantString(); + + //Act + await fixture.RedmineManager.DeleteAsync(id); + + await Task.Delay(200); + + //Assert + await Assert.ThrowsAsync(TestCode); + return; + + async Task TestCode() + { + await fixture.RedmineManager.GetAsync(id); + } + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/ProjectMembership/ProjectMembershipTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/ProjectMembership/ProjectMembershipTests.cs new file mode 100644 index 00000000..2b2b2f52 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/ProjectMembership/ProjectMembershipTests.cs @@ -0,0 +1,84 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.ProjectMembership; + +[Collection(Constants.RedmineTestContainerCollection)] +public class ProjectMembershipTests(RedmineTestContainerFixture fixture) +{ + private Redmine.Net.Api.Types.ProjectMembership CreateRandomProjectMembership() + { + var roles = fixture.RedmineManager.Get(); + Assert.NotEmpty(roles); + + var user = TestEntityFactory.CreateRandomUserPayload(); + var createdUser = fixture.RedmineManager.Create(user); + Assert.NotNull(createdUser); + + var membership = new Redmine.Net.Api.Types.ProjectMembership + { + User = new IdentifiableName { Id = createdUser.Id }, + Roles = [new MembershipRole { Id = roles[0].Id }] + }; + + return fixture.RedmineManager.Create(membership, TestConstants.Projects.DefaultProjectIdentifier); + } + + [Fact] + public void GetProjectMemberships_WithValidProjectId_ShouldReturnMemberships() + { + var memberships = fixture.RedmineManager.GetProjectMemberships(TestConstants.Projects.DefaultProjectIdentifier); + Assert.NotNull(memberships); + } + + [Fact] + public void CreateProjectMembership_WithValidData_ShouldSucceed() + { + var membership = CreateRandomProjectMembership(); + + Assert.NotNull(membership); + Assert.True(membership.Id > 0); + Assert.NotNull(membership.User); + Assert.NotEmpty(membership.Roles); + } + + [Fact] + public void UpdateProjectMembership_WithValidData_ShouldSucceed() + { + var membership = CreateRandomProjectMembership(); + Assert.NotNull(membership); + + var roles = fixture.RedmineManager.Get(); + Assert.NotEmpty(roles); + + var newRoleId = roles.First(r => membership.Roles.All(mr => mr.Id != r.Id)).Id; + membership.Roles = [new MembershipRole { Id = newRoleId }]; + + // Act + fixture.RedmineManager.Update(membership.Id.ToString(), membership); + + var updatedMembership = fixture.RedmineManager.Get(membership.Id.ToString()); + + // Assert + Assert.NotNull(updatedMembership); + Assert.Contains(updatedMembership.Roles, r => r.Id == newRoleId); + } + + [Fact] + public void DeleteProjectMembership_WithValidId_ShouldSucceed() + { + // Arrange + var membership = CreateRandomProjectMembership(); + Assert.NotNull(membership); + + // Act + fixture.RedmineManager.Delete(membership.Id.ToString()); + + // Assert + Assert.Throws(() => fixture.RedmineManager.Get(membership.Id.ToString())); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/ProjectMembership/ProjectMembershipTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/ProjectMembership/ProjectMembershipTestsAsync.cs new file mode 100644 index 00000000..e8c36536 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/ProjectMembership/ProjectMembershipTestsAsync.cs @@ -0,0 +1,123 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.ProjectMembership; + +[Collection(Constants.RedmineTestContainerCollection)] +public class ProjectMembershipTestsAsync(RedmineTestContainerFixture fixture) +{ + private async Task CreateRandomMembershipAsync() + { + var roles = await fixture.RedmineManager.GetAsync(); + Assert.NotEmpty(roles); + + var userPayload = TestEntityFactory.CreateRandomUserPayload(); + var user = await fixture.RedmineManager.CreateAsync(userPayload); + Assert.NotNull(user); + + var membership = new Redmine.Net.Api.Types.ProjectMembership + { + User = new IdentifiableName { Id = user.Id }, + Roles = [new MembershipRole { Id = roles[0].Id }] + }; + + return await fixture.RedmineManager.CreateAsync(membership, TestConstants.Projects.DefaultProjectIdentifier); + } + + [Fact] + public async Task GetProjectMemberships_WithValidProjectId_ShouldReturnMemberships() + { + // Act + var memberships = await fixture.RedmineManager.GetProjectMembershipsAsync(TestConstants.Projects.DefaultProjectIdentifier); + + // Assert + Assert.NotNull(memberships); + } + + [Fact] + public async Task CreateProjectMembership_WithValidData_ShouldSucceed() + { + // Arrange & Act + var projectMembership = await CreateRandomMembershipAsync(); + + // Assert + Assert.NotNull(projectMembership); + Assert.True(projectMembership.Id > 0); + Assert.NotNull(projectMembership.User); + Assert.NotEmpty(projectMembership.Roles); + } + + [Fact] + public async Task UpdateProjectMembership_WithValidData_ShouldSucceed() + { + // Arrange + var membership = await CreateRandomMembershipAsync(); + Assert.NotNull(membership); + + var roles = await fixture.RedmineManager.GetAsync(); + Assert.NotEmpty(roles); + + var newRoleId = roles.FirstOrDefault(r => membership.Roles.All(mr => mr.Id != r.Id))?.Id ?? roles.First().Id; + membership.Roles = [new MembershipRole { Id = newRoleId }]; + + // Act + await fixture.RedmineManager.UpdateAsync(membership.Id.ToString(), membership); + + var updatedMembership = await fixture.RedmineManager.GetAsync(membership.Id.ToString()); + + // Assert + Assert.NotNull(updatedMembership); + Assert.Contains(updatedMembership.Roles, r => r.Id == newRoleId); + } + + [Fact] + public async Task DeleteProjectMembership_WithValidId_ShouldSucceed() + { + // Arrange + var membership = await CreateRandomMembershipAsync(); + Assert.NotNull(membership); + + var membershipId = membership.Id.ToString(); + + // Act + await fixture.RedmineManager.DeleteAsync(membershipId); + + // Assert + await Assert.ThrowsAsync(() => fixture.RedmineManager.GetAsync(membershipId)); + } + + [Fact] + public async Task GetProjectMemberships_ShouldReturnMemberships() + { + // Test implementation + } + + [Fact] + public async Task GetProjectMembership_WithValidId_ShouldReturnMembership() + { + // Test implementation + } + + [Fact] + public async Task CreateProjectMembership_WithInvalidData_ShouldFail() + { + // Test implementation + } + + [Fact] + public async Task UpdateProjectMembership_WithInvalidData_ShouldFail() + { + // Test implementation + } + + [Fact] + public async Task DeleteProjectMembership_WithInvalidId_ShouldFail() + { + // Test implementation + } + +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Query/QueryTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Query/QueryTests.cs new file mode 100644 index 00000000..b4af0e84 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Query/QueryTests.cs @@ -0,0 +1,18 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Query; + +[Collection(Constants.RedmineTestContainerCollection)] +public class QueryTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void GetAllQueries_Should_Succeed() + { + // Act + var queries = fixture.RedmineManager.Get(); + + // Assert + Assert.NotNull(queries); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Query/QueryTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Query/QueryTestsAsync.cs new file mode 100644 index 00000000..c8bb6d16 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Query/QueryTestsAsync.cs @@ -0,0 +1,18 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Query; + +[Collection(Constants.RedmineTestContainerCollection)] +public class QueryTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task GetAllQueries_Should_Succeed() + { + // Act + var queries = await fixture.RedmineManager.GetAsync(); + + // Assert + Assert.NotNull(queries); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Role/RoleTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Role/RoleTests.cs new file mode 100644 index 00000000..165c1c4f --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Role/RoleTests.cs @@ -0,0 +1,19 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Role; + +[Collection(Constants.RedmineTestContainerCollection)] +public class RoleTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void Get_All_Roles_Should_Succeed() + { + //Act + var roles = fixture.RedmineManager.Get(); + + //Assert + Assert.NotNull(roles); + Assert.NotEmpty(roles); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Role/RoleTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Role/RoleTestsAsync.cs new file mode 100644 index 00000000..fb1dbd52 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Role/RoleTestsAsync.cs @@ -0,0 +1,19 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Role; + +[Collection(Constants.RedmineTestContainerCollection)] +public class RoleTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task Get_All_Roles_Should_Succeed() + { + //Act + var roles = await fixture.RedmineManager.GetAsync(); + + //Assert + Assert.NotNull(roles); + Assert.NotEmpty(roles); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Search/SearchTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Search/SearchTests.cs new file mode 100644 index 00000000..0f4fc789 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Search/SearchTests.cs @@ -0,0 +1,28 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api; +using Redmine.Net.Api.Extensions; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Search; + +[Collection(Constants.RedmineTestContainerCollection)] +public class SearchTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void Search_Should_Succeed() + { + // Arrange + var searchBuilder = new SearchFilterBuilder + { + IncludeIssues = true, + IncludeWikiPages = true + }; + + // Act + var results = fixture.RedmineManager.Search("query_string",100, searchFilter:searchBuilder); + + // Assert + Assert.NotNull(results); + Assert.Null(results.Items); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Search/SearchTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Search/SearchTestsAsync.cs new file mode 100644 index 00000000..ba9f151e --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Search/SearchTestsAsync.cs @@ -0,0 +1,28 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api; +using Redmine.Net.Api.Extensions; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Search; + +[Collection(Constants.RedmineTestContainerCollection)] +public class SearchTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task Search_Should_Succeed() + { + // Arrange + var searchBuilder = new SearchFilterBuilder + { + IncludeIssues = true, + IncludeWikiPages = true + }; + + // Act + var results = await fixture.RedmineManager.SearchAsync("query_string",100, searchFilter:searchBuilder); + + // Assert + Assert.NotNull(results); + Assert.Null(results.Items); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/TimeEntry/TimeEntryActivityTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/TimeEntry/TimeEntryActivityTestsAsync.cs new file mode 100644 index 00000000..b2e9546b --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/TimeEntry/TimeEntryActivityTestsAsync.cs @@ -0,0 +1,20 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.TimeEntry; + +[Collection(Constants.RedmineTestContainerCollection)] +public class TimeEntryActivityTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task GetAllTimeEntryActivities_Should_Succeed() + { + // Act + var activities = await fixture.RedmineManager.GetAsync(); + + // Assert + Assert.NotNull(activities); + Assert.NotEmpty(activities); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/TimeEntry/TimeEntryTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/TimeEntry/TimeEntryTestsAsync.cs new file mode 100644 index 00000000..db3719e1 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/TimeEntry/TimeEntryTestsAsync.cs @@ -0,0 +1,91 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.TimeEntry; + +[Collection(Constants.RedmineTestContainerCollection)] +public class TimeEntryTestsAsync(RedmineTestContainerFixture fixture) +{ + private async Task<(Redmine.Net.Api.Types.TimeEntry, Redmine.Net.Api.Types.TimeEntry payload)> CreateRandomTestTimeEntryAsync() + { + var (issue, _) = await IssueTestHelper.CreateRandomIssueAsync(fixture.RedmineManager); + + var timeEntry = TestEntityFactory.CreateRandomTimeEntryPayload(TestConstants.Projects.DefaultProjectId, issue.Id, activityId: 8); + return (await fixture.RedmineManager.CreateAsync(timeEntry), timeEntry); + } + + [Fact] + public async Task CreateTimeEntry_Should_Succeed() + { + //Arrange & Act + var (timeEntry, timeEntryPayload) = await CreateRandomTestTimeEntryAsync(); + + //Assert + Assert.NotNull(timeEntry); + Assert.True(timeEntry.Id > 0); + Assert.Equal(timeEntryPayload.Hours, timeEntry.Hours); + Assert.Equal(timeEntryPayload.Comments, timeEntry.Comments); + Assert.Equal(timeEntryPayload.Project.Id, timeEntry.Project.Id); + Assert.Equal(timeEntryPayload.Issue.Id, timeEntry.Issue.Id); + Assert.Equal(timeEntryPayload.Activity.Id, timeEntry.Activity.Id); + } + + [Fact] + public async Task GetTimeEntry_Should_Succeed() + { + //Arrange + var (createdTimeEntry,_) = await CreateRandomTestTimeEntryAsync(); + Assert.NotNull(createdTimeEntry); + + //Act + var retrievedTimeEntry = await fixture.RedmineManager.GetAsync(createdTimeEntry.Id.ToInvariantString()); + + //Assert + Assert.NotNull(retrievedTimeEntry); + Assert.Equal(createdTimeEntry.Id, retrievedTimeEntry.Id); + Assert.Equal(createdTimeEntry.Hours, retrievedTimeEntry.Hours); + Assert.Equal(createdTimeEntry.Comments, retrievedTimeEntry.Comments); + } + + [Fact] + public async Task UpdateTimeEntry_Should_Succeed() + { + //Arrange + var (createdTimeEntry,_) = await CreateRandomTestTimeEntryAsync(); + Assert.NotNull(createdTimeEntry); + + var updatedComments = $"Updated test time entry comments {Guid.NewGuid()}"; + var updatedHours = 2.5m; + createdTimeEntry.Comments = updatedComments; + createdTimeEntry.Hours = updatedHours; + + //Act + await fixture.RedmineManager.UpdateAsync(createdTimeEntry.Id.ToInvariantString(), createdTimeEntry); + var retrievedTimeEntry = await fixture.RedmineManager.GetAsync(createdTimeEntry.Id.ToInvariantString()); + + //Assert + Assert.NotNull(retrievedTimeEntry); + Assert.Equal(createdTimeEntry.Id, retrievedTimeEntry.Id); + Assert.Equal(updatedComments, retrievedTimeEntry.Comments); + Assert.Equal(updatedHours, retrievedTimeEntry.Hours); + } + + [Fact] + public async Task DeleteTimeEntry_Should_Succeed() + { + //Arrange + var (createdTimeEntry,_) = await CreateRandomTestTimeEntryAsync(); + Assert.NotNull(createdTimeEntry); + + var timeEntryId = createdTimeEntry.Id.ToInvariantString(); + + //Act + await fixture.RedmineManager.DeleteAsync(timeEntryId); + + //Assert + await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetAsync(timeEntryId)); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Tracker/TrackerTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Tracker/TrackerTestsAsync.cs new file mode 100644 index 00000000..c09016c9 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Tracker/TrackerTestsAsync.cs @@ -0,0 +1,19 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Tracker; + +[Collection(Constants.RedmineTestContainerCollection)] +public class TrackerTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task Get_All_Trackers_Should_Succeed() + { + //Act + var trackers = await fixture.RedmineManager.GetAsync(); + + //Assert + Assert.NotNull(trackers); + Assert.NotEmpty(trackers); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/UploadTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/UploadTestsAsync.cs new file mode 100644 index 00000000..4cd293cf --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/UploadTestsAsync.cs @@ -0,0 +1,87 @@ +using System.Text; +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities; + +[Collection(Constants.RedmineTestContainerCollection)] +public class UploadTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task Upload_Attachment_To_Issue_Should_Succeed() + { + var bytes = "Hello World!"u8.ToArray(); + var uploadFile = await fixture.RedmineManager.UploadFileAsync(bytes, "hello-world.txt"); + + Assert.NotNull(uploadFile); + Assert.NotNull(uploadFile.Token); + + var issue = await fixture.RedmineManager.CreateAsync(new Redmine.Net.Api.Types.Issue() + { + Project = 1.ToIdentifier(), + Subject = "Creating an issue with a uploaded file", + Tracker = 1.ToIdentifier(), + Status = 1.ToIssueStatusIdentifier(), + Uploads = [ + new Upload() + { + Token = uploadFile.Token, + ContentType = "text/plain", + Description = "An optional description here", + FileName = "hello-world.txt" + } + ] + }); + + Assert.NotNull(issue); + + var files = await fixture.RedmineManager.GetAsync(issue.Id.ToInvariantString(), RequestOptions.Include(RedmineKeys.ATTACHMENTS)); + + Assert.NotNull(files); + Assert.Single(files.Attachments); + } + + [Fact] + public async Task Upload_Attachment_To_Wiki_Should_Succeed() + { + var bytes = Encoding.UTF8.GetBytes(RandomHelper.GenerateText("Hello Wiki!",10)); + var fileName = $"{RandomHelper.GenerateText("wiki-",5)}.txt"; + var uploadFile = await fixture.RedmineManager.UploadFileAsync(bytes, fileName); + + Assert.NotNull(uploadFile); + Assert.NotNull(uploadFile.Token); + + var wikiPageName = RandomHelper.GenerateText(7); + + var wikiPageInfo = new WikiPage() + { + Version = 0, + Comments = RandomHelper.GenerateText(15), + Text = RandomHelper.GenerateText(10), + Uploads = + [ + new Upload() + { + Token = uploadFile.Token, + ContentType = "text/plain", + Description = RandomHelper.GenerateText(15), + FileName = fileName, + } + ] + }; + + var wiki = await fixture.RedmineManager.CreateWikiPageAsync(1.ToInvariantString(), wikiPageName, wikiPageInfo); + + Assert.NotNull(wiki); + + var files = await fixture.RedmineManager.GetWikiPageAsync(1.ToInvariantString(), wikiPageName, RequestOptions.Include(RedmineKeys.ATTACHMENTS)); + + Assert.NotNull(files); + Assert.Single(files.Attachments); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/User/UserTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/User/UserTestsAsync.cs new file mode 100644 index 00000000..cd63378b --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/User/UserTestsAsync.cs @@ -0,0 +1,243 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; +using Redmine.Net.Api; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Http.Extensions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.User; + +[Collection(Constants.RedmineTestContainerCollection)] +public class UserTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task CreateUser_WithValidData_ShouldSucceed() + { + //Arrange + var userPayload = TestEntityFactory.CreateRandomUserPayload(emailNotificationType: EmailNotificationType.OnlyMyEvents); + + //Act + var createdUser = await fixture.RedmineManager.CreateAsync(userPayload); + + //Assert + Assert.NotNull(createdUser); + Assert.True(createdUser.Id > 0); + Assert.Equal(userPayload.Login, createdUser.Login); + Assert.Equal(userPayload.FirstName, createdUser.FirstName); + Assert.Equal(userPayload.LastName, createdUser.LastName); + Assert.Equal(userPayload.Email, createdUser.Email); + } + + [Fact] + public async Task GetUser_WithValidId_ShouldReturnUser() + { + //Arrange + var userPayload = TestEntityFactory.CreateRandomUserPayload(); + var user = await fixture.RedmineManager.CreateAsync(userPayload); + Assert.NotNull(user); + + //Act + var retrievedUser = await fixture.RedmineManager.GetAsync(user.Id.ToInvariantString()); + + //Assert + Assert.NotNull(retrievedUser); + Assert.Equal(user.Id, retrievedUser.Id); + Assert.Equal(user.Login, retrievedUser.Login); + Assert.Equal(user.FirstName, retrievedUser.FirstName); + } + + [Fact] + public async Task UpdateUser_WithValidData_ShouldSucceed() + { + //Arrange + var userPayload = TestEntityFactory.CreateRandomUserPayload(); + var user = await fixture.RedmineManager.CreateAsync(userPayload); + Assert.NotNull(user); + + user.FirstName = RandomHelper.GenerateText(10); + + //Act + await fixture.RedmineManager.UpdateAsync(user.Id.ToInvariantString(), user); + var retrievedUser = await fixture.RedmineManager.GetAsync(user.Id.ToInvariantString()); + + //Assert + Assert.NotNull(retrievedUser); + Assert.Equal(user.Id, retrievedUser.Id); + Assert.Equal(user.FirstName, retrievedUser.FirstName); + } + + [Fact] + public async Task DeleteUser_WithValidId_ShouldSucceed() + { + //Arrange + var userPayload = TestEntityFactory.CreateRandomUserPayload(); + var user = await fixture.RedmineManager.CreateAsync(userPayload); + Assert.NotNull(user); + + var userId = user.Id.ToInvariantString(); + + //Act + await fixture.RedmineManager.DeleteAsync(userId); + + //Assert + await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetAsync(userId)); + } + + [Fact] + public async Task GetCurrentUser_ShouldReturnUserDetails() + { + var currentUser = await fixture.RedmineManager.GetCurrentUserAsync(); + Assert.NotNull(currentUser); + } + + [Fact] + public async Task GetUsers_WithActiveStatus_ShouldReturnUsers() + { + var userPayload = TestEntityFactory.CreateRandomUserPayload(); + var user = await fixture.RedmineManager.CreateAsync(userPayload); + Assert.NotNull(user); + + var users = await fixture.RedmineManager.GetAsync(new RequestOptions() + { + QueryString = RedmineKeys.STATUS.WithItem(((int)UserStatus.StatusActive).ToString()) + }); + + Assert.NotNull(users); + Assert.True(users.Count > 0, "User count == 0"); + } + + [Fact] + public async Task GetUsers_WithLockedStatus_ShouldReturnUsers() + { + var userPayload = TestEntityFactory.CreateRandomUserPayload(status: UserStatus.StatusLocked); + var user = await fixture.RedmineManager.CreateAsync(userPayload); + Assert.NotNull(user); + + var users = await fixture.RedmineManager.GetAsync(new RequestOptions() + { + QueryString = RedmineKeys.STATUS.WithItem(((int)UserStatus.StatusLocked).ToString()) + }); + + Assert.NotNull(users); + Assert.True(users.Count >= 1, "User(Locked) count == 0"); + } + + [Fact] + public async Task GetUsers_WithRegisteredStatus_ShouldReturnUsers() + { + var userPayload = TestEntityFactory.CreateRandomUserPayload(status: UserStatus.StatusRegistered); + var user = await fixture.RedmineManager.CreateAsync(userPayload); + Assert.NotNull(user); + + var users = await fixture.RedmineManager.GetAsync(new RequestOptions() + { + QueryString = RedmineKeys.STATUS.WithInt((int)UserStatus.StatusRegistered) + }); + + Assert.NotNull(users); + Assert.True(users.Count >= 1, "User(Registered) count == 0"); + } + + [Fact] + public async Task GetUser_WithGroupsAndMemberships_ShouldIncludeRelatedData() + { + var roles = await fixture.RedmineManager.GetAsync(); + Assert.NotEmpty(roles); + + var userPayload = TestEntityFactory.CreateRandomUserPayload(); + var user = await fixture.RedmineManager.CreateAsync(userPayload); + Assert.NotNull(user); + + var membership = new Redmine.Net.Api.Types.ProjectMembership + { + User = new IdentifiableName { Id = user.Id }, + Roles = [new MembershipRole { Id = roles[0].Id }] + }; + + var groupPayload = new Redmine.Net.Api.Types.Group() + { + Name = RandomHelper.GenerateText(3), + Users = [IdentifiableName.Create(user.Id)] + }; + + var group = await fixture.RedmineManager.CreateAsync(groupPayload); + Assert.NotNull(group); + + // Act + var projectMembership = await fixture.RedmineManager.CreateAsync(membership, TestConstants.Projects.DefaultProjectIdentifier); + Assert.NotNull(projectMembership); + + user = await fixture.RedmineManager.GetAsync(user.Id.ToInvariantString(), + RequestOptions.Include($"{RedmineKeys.GROUPS},{RedmineKeys.MEMBERSHIPS}")); + + Assert.NotNull(user); + Assert.NotNull(user.Groups); + Assert.NotNull(user.Memberships); + + Assert.True(user.Groups.Count > 0, "Group count == 0"); + Assert.True(user.Memberships.Count > 0, "Membership count == 0"); + } + + [Fact] + public async Task GetUsers_ByGroupId_ShouldReturnFilteredUsers() + { + var userPayload = TestEntityFactory.CreateRandomUserPayload(); + var user = await fixture.RedmineManager.CreateAsync(userPayload); + Assert.NotNull(user); + + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: [user.Id]); + var group = await fixture.RedmineManager.CreateAsync(groupPayload); + Assert.NotNull(group); + + var users = await fixture.RedmineManager.GetAsync(new RequestOptions() + { + QueryString = RedmineKeys.GROUP_ID.WithInt(group.Id) + }); + + Assert.NotNull(users); + Assert.True(users.Count > 0, "User count == 0"); + } + + [Fact] + public async Task AddUserToGroup_WithValidIds_ShouldSucceed() + { + var userPayload = TestEntityFactory.CreateRandomUserPayload(); + var user = await fixture.RedmineManager.CreateAsync(userPayload); + Assert.NotNull(user); + + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(name: null, userIds: null); + var group = await fixture.RedmineManager.CreateAsync(groupPayload); + Assert.NotNull(group); + + await fixture.RedmineManager.AddUserToGroupAsync(group.Id, user.Id); + + user = fixture.RedmineManager.Get(user.Id.ToInvariantString(), RequestOptions.Include(RedmineKeys.GROUPS)); + + Assert.NotNull(user); + Assert.NotNull(user.Groups); + Assert.NotNull(user.Groups.FirstOrDefault(g => g.Id == group.Id)); + } + + [Fact] + public async Task RemoveUserFromGroup_WithValidIds_ShouldSucceed() + { + var userPayload = TestEntityFactory.CreateRandomUserPayload(); + var user = await fixture.RedmineManager.CreateAsync(userPayload); + Assert.NotNull(user); + + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: [user.Id]); + var group = await fixture.RedmineManager.CreateAsync(groupPayload); + Assert.NotNull(group); + + await fixture.RedmineManager.RemoveUserFromGroupAsync(group.Id, user.Id); + + user = await fixture.RedmineManager.GetAsync(user.Id.ToInvariantString(), RequestOptions.Include(RedmineKeys.GROUPS)); + + Assert.NotNull(user); + Assert.True(user.Groups == null || user.Groups.FirstOrDefault(g => g.Id == group.Id) == null); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Version/VersionTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Version/VersionTestsAsync.cs new file mode 100644 index 00000000..e292bf4d --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Version/VersionTestsAsync.cs @@ -0,0 +1,87 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Version; + +[Collection(Constants.RedmineTestContainerCollection)] +public class VersionTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task CreateVersion_Should_Succeed() + { + // Arrange + var versionPayload = TestEntityFactory.CreateRandomVersionPayload(); + + // Act + var version = await fixture.RedmineManager.CreateAsync(versionPayload, TestConstants.Projects.DefaultProjectIdentifier); + + // Assert + Assert.NotNull(version); + Assert.True(version.Id > 0); + Assert.Equal(versionPayload.Name, version.Name); + Assert.Equal(versionPayload.Description, version.Description); + Assert.Equal(versionPayload.Status, version.Status); + Assert.Equal(TestConstants.Projects.DefaultProjectId, version.Project.Id); + } + + [Fact] + public async Task GetVersion_Should_Succeed() + { + // Arrange + var versionPayload = TestEntityFactory.CreateRandomVersionPayload(); + var version = await fixture.RedmineManager.CreateAsync(versionPayload, TestConstants.Projects.DefaultProjectIdentifier); + Assert.NotNull(version); + + // Act + var retrievedVersion = await fixture.RedmineManager.GetAsync(version.Id.ToInvariantString()); + + // Assert + Assert.NotNull(retrievedVersion); + Assert.Equal(version.Id, retrievedVersion.Id); + Assert.Equal(version.Name, retrievedVersion.Name); + Assert.Equal(version.Description, retrievedVersion.Description); + } + + [Fact] + public async Task UpdateVersion_Should_Succeed() + { + // Arrange + var versionPayload = TestEntityFactory.CreateRandomVersionPayload(); + var version = await fixture.RedmineManager.CreateAsync(versionPayload, TestConstants.Projects.DefaultProjectIdentifier); + Assert.NotNull(version); + + version.Description = RandomHelper.GenerateText(20); + version.Status = VersionStatus.Locked; + + // Act + await fixture.RedmineManager.UpdateAsync(version.Id.ToString(), version); + var retrievedVersion = await fixture.RedmineManager.GetAsync(version.Id.ToInvariantString()); + + // Assert + Assert.NotNull(retrievedVersion); + Assert.Equal(version.Id, retrievedVersion.Id); + Assert.Equal(version.Description, retrievedVersion.Description); + Assert.Equal(version.Status, retrievedVersion.Status); + } + + [Fact] + public async Task DeleteVersion_Should_Succeed() + { + // Arrange + var versionPayload = TestEntityFactory.CreateRandomVersionPayload(); + var version = await fixture.RedmineManager.CreateAsync(versionPayload, TestConstants.Projects.DefaultProjectIdentifier); + Assert.NotNull(version); + + // Act + await fixture.RedmineManager.DeleteAsync(version); + + // Assert + await Assert.ThrowsAsync(async () => + await fixture.RedmineManager.GetAsync(version)); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Wiki/WikiTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Wiki/WikiTestsAsync.cs new file mode 100644 index 00000000..c5033eb3 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Wiki/WikiTestsAsync.cs @@ -0,0 +1,207 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; +using Redmine.Net.Api; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Wiki; + +[Collection(Constants.RedmineTestContainerCollection)] +public class WikiTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task CreateWikiPage_WithValidData_ShouldSucceed() + { + // Arrange + var (pageName, wikiPagePayload) = TestEntityFactory.CreateRandomWikiPagePayload(); + + // Act + var wikiPage = await fixture.RedmineManager.CreateWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, pageName, wikiPagePayload); + + // Assert + Assert.NotNull(wikiPage); + } + + [Fact] + public async Task GetWikiPage_WithValidTitle_ShouldReturnPage() + { + // Arrange + var (pageName, wikiPagePayload) = TestEntityFactory.CreateRandomWikiPagePayload(); + + // Act + var wikiPage = await fixture.RedmineManager.CreateWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, pageName, wikiPagePayload); + Assert.NotNull(wikiPage); + + var retrievedPage = await fixture.RedmineManager.GetWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, wikiPage.Title); + + // Assert + Assert.NotNull(retrievedPage); + Assert.Equal(pageName, retrievedPage.Title); + Assert.Equal(wikiPage.Text, retrievedPage.Text); + } + + [Fact] + public async Task GetAllWikiPages_ForValidProject_ShouldReturnPages() + { + // Arrange + var (firstPageName, firstWikiPagePayload) = TestEntityFactory.CreateRandomWikiPagePayload(); + var (secondPageName, secondWikiPagePayload) = TestEntityFactory.CreateRandomWikiPagePayload(); + + // Act + var wikiPage = await fixture.RedmineManager.CreateWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, firstPageName, firstWikiPagePayload); + Assert.NotNull(wikiPage); + + var wikiPage2 = await fixture.RedmineManager.CreateWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, secondPageName, secondWikiPagePayload); + Assert.NotNull(wikiPage2); + + // Act + var wikiPages = await fixture.RedmineManager.GetAllWikiPagesAsync(TestConstants.Projects.DefaultProjectIdentifier); + + // Assert + Assert.NotNull(wikiPages); + Assert.NotEmpty(wikiPages); + } + + [Fact] + public async Task DeleteWikiPage_WithValidTitle_ShouldSucceed() + { + // Arrange + var (pageName, wikiPagePayload) = TestEntityFactory.CreateRandomWikiPagePayload(); + + // Act + var wikiPage = await fixture.RedmineManager.CreateWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, pageName, wikiPagePayload); + Assert.NotNull(wikiPage); + + // Act + await fixture.RedmineManager.DeleteWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, wikiPage.Title); + + // Assert + await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, wikiPage.Title)); + } + + [Fact] + public async Task CreateWikiPage_Should_Succeed() + { + // Arrange + var (pageName, wikiPagePayload) = TestEntityFactory.CreateRandomWikiPagePayload(); + + // Act + var wikiPage = await fixture.RedmineManager.CreateWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, pageName, wikiPagePayload); + + // Assert + Assert.NotNull(wikiPage); + Assert.NotNull(wikiPage.Author); + Assert.NotNull(wikiPage.CreatedOn); + Assert.Equal(DateTime.Now, wikiPage.CreatedOn.Value, TimeSpan.FromSeconds(5)); + Assert.Equal(pageName, wikiPage.Title); + Assert.Equal(wikiPagePayload.Text, wikiPage.Text); + Assert.Equal(1, wikiPage.Version); + } + + [Fact] + public async Task UpdateWikiPage_WithValidData_ShouldSucceed() + { + // Arrange + var (pageName, wikiPagePayload) = TestEntityFactory.CreateRandomWikiPagePayload(); + + var wikiPage = await fixture.RedmineManager.CreateWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, pageName, wikiPagePayload); + Assert.NotNull(wikiPage); + + wikiPage.Text = "Updated wiki text content"; + wikiPage.Comments = "These are updated comments for the wiki page update."; + + // Act + await fixture.RedmineManager.UpdateWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, pageName, wikiPage); + + var retrievedPage = await fixture.RedmineManager.GetWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, wikiPage.Title); + + // Assert + Assert.NotNull(retrievedPage); + Assert.Equal(wikiPage.Text, retrievedPage.Text); + Assert.Equal(wikiPage.Comments, retrievedPage.Comments); + } + + [Fact] + public async Task GetWikiPage_WithNameAndAttachments_ShouldReturnCompleteData() + { + // Arrange + var fileUpload = await FileTestHelper.UploadRandom500KbFileAsync(fixture.RedmineManager); + Assert.NotNull(fileUpload); + Assert.NotEmpty(fileUpload.Token); + + fileUpload.ContentType = "text/plain"; + fileUpload.Description = RandomHelper.GenerateText(15); + fileUpload.FileName = "hello-world.txt"; + + var (pageName, wikiPagePayload) = TestEntityFactory.CreateRandomWikiPagePayload(pageName: RandomHelper.GenerateText(prefix: "Te$t"), uploads: [fileUpload]); + + var wikiPage = await fixture.RedmineManager.CreateWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, pageName, wikiPagePayload); + Assert.NotNull(wikiPage); + + // Act + var page = await fixture.RedmineManager.GetWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, pageName, RequestOptions.Include(RedmineKeys.ATTACHMENTS)); + + // Assert + Assert.NotNull(page); + Assert.Equal(pageName, page.Title); + Assert.NotNull(page.Comments); + Assert.NotNull(page.Author); + Assert.NotNull(page.CreatedOn); + Assert.Equal(DateTime.Now, page.CreatedOn.Value, TimeSpan.FromSeconds(5)); + + Assert.NotNull(page.Attachments); + Assert.NotEmpty(page.Attachments); + + var attachment = page.Attachments.FirstOrDefault(x => x.FileName == fileUpload.FileName); + Assert.NotNull(attachment); + Assert.Equal("text/plain", attachment.ContentType); + Assert.NotNull(attachment.Description); + Assert.Equal(attachment.FileName, attachment.FileName); + Assert.EndsWith($"/attachments/download/{attachment.Id}/{attachment.FileName}", attachment.ContentUrl); + Assert.True(attachment.FileSize > 0); + } + + [Fact] + public async Task GetWikiPage_WithOldVersion_ShouldReturnHistoricalData() + { + //Arrange + var (pageName, wikiPagePayload) = TestEntityFactory.CreateRandomWikiPagePayload(); + + var wikiPage = await fixture.RedmineManager.CreateWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, pageName, wikiPagePayload); + Assert.NotNull(wikiPage); + + wikiPage.Text = RandomHelper.GenerateText(8); + wikiPage.Comments = RandomHelper.GenerateText(9); + + // Act + await fixture.RedmineManager.UpdateWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, pageName, wikiPage); + + var oldPage = await fixture.RedmineManager.GetWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, wikiPage.Title, version: 1); + + // Assert + Assert.NotNull(oldPage); + Assert.Equal(wikiPagePayload.Text, oldPage.Text); + Assert.Equal(wikiPagePayload.Comments, oldPage.Comments); + Assert.Equal(1, oldPage.Version); + } + + [Fact] + public async Task GetWikiPage_WithSpecialChars_ShouldReturnPage() + { + //Arrange + var (pageName, wikiPagePayload) = TestEntityFactory.CreateRandomWikiPagePayload(pageName: "some-page-with-umlauts-and-other-special-chars-Γ€ΓΆΓΌΓ„Γ–ΓœΓŸ"); + + var wikiPage = await fixture.RedmineManager.CreateWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, pageName, wikiPagePayload); + Assert.Null(wikiPage); //it seems that Redmine returns 204 (No content) when the page name contains special characters + + // Act + var page = await fixture.RedmineManager.GetWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, pageName); + + // Assert + Assert.NotNull(page); + Assert.Equal(pageName, page.Title); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Progress/ProgressTests.Async.cs b/tests/redmine-net-api.Integration.Tests/Tests/Progress/ProgressTests.Async.cs new file mode 100644 index 00000000..2849cd37 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Progress/ProgressTests.Async.cs @@ -0,0 +1,60 @@ +using Redmine.Net.Api.Exceptions; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Progress; + +public partial class ProgressTests +{ + [Fact] + public async Task DownloadFileAsync_WithValidUrl_ShouldReportProgress() + { + // Arrange + var progressTracker = new ProgressTracker(); + + // Act + var result = await fixture.RedmineManager.DownloadFileAsync( + "",null, + progressTracker, + CancellationToken.None); + + // Assert + Assert.NotNull(result); + Assert.True(result.Length > 0, "Downloaded content should not be empty"); + + AssertProgressWasReported(progressTracker); + } + + [Fact] + public async Task DownloadFileAsync_WithCancellation_ShouldStopDownload() + { + // Arrange + var progressTracker = new ProgressTracker(); + var cts = new CancellationTokenSource(); + + try + { + progressTracker.OnProgressReported += (sender, args) => + { + if (args.Value > 0 && !cts.IsCancellationRequested) + { + cts.Cancel(); + } + }; + + // Act & Assert + await Assert.ThrowsAnyAsync(async () => + { + await fixture.RedmineManager.DownloadFileAsync( + "", + null, + progressTracker, + cts.Token); + }); + + Assert.True(progressTracker.ReportCount > 0, "Progress should have been reported at least once"); + } + finally + { + cts.Dispose(); + } + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Progress/ProgressTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Progress/ProgressTests.cs new file mode 100644 index 00000000..b9ed86a9 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Progress/ProgressTests.cs @@ -0,0 +1,56 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Progress; + +[Collection(Constants.RedmineTestContainerCollection)] +public partial class ProgressTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void DownloadFile_WithValidUrl_ShouldReportProgress() + { + // Arrange + var progressTracker = new ProgressTracker(); + + // Act + var result = fixture.RedmineManager.DownloadFile("", progressTracker); + + // Assert + Assert.NotNull(result); + Assert.True(result.Length > 0, "Downloaded content should not be empty"); + + AssertProgressWasReported(progressTracker); + } + + private static void AssertProgressWasReported(ProgressTracker tracker) + { + Assert.True(tracker.ReportCount > 0, "Progress should have been reported at least once"); + + Assert.Contains(100, tracker.ProgressValues); + + for (var i = 0; i < tracker.ProgressValues.Count - 1; i++) + { + Assert.True(tracker.ProgressValues[i] <= tracker.ProgressValues[i + 1], + $"Progress should not decrease: {tracker.ProgressValues[i]} -> {tracker.ProgressValues[i + 1]}"); + } + } + + private sealed class ProgressTracker : IProgress + { + public List ProgressValues { get; } = []; + public int ReportCount => ProgressValues.Count; + + public event EventHandler OnProgressReported; + + public void Report(int value) + { + ProgressValues.Add(value); + OnProgressReported?.Invoke(this, new ProgressReportedEventArgs(value)); + } + + public sealed class ProgressReportedEventArgs(int value) : EventArgs + { + public int Value { get; } = value; + } + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/RedmineApiWebClientTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/RedmineApiWebClientTests.cs new file mode 100644 index 00000000..ebed01e9 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/RedmineApiWebClientTests.cs @@ -0,0 +1,75 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests; + +[Collection(Constants.RedmineTestContainerCollection)] +public class RedmineApiWebClientTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task SendAsync_WhenRequestCanceled_ThrowsRedmineOperationCanceledException() + { + using var cts = new CancellationTokenSource(); + // Arrange + cts.CancelAfter(TimeSpan.FromMilliseconds(100)); + + // Act & Assert + _ = await Assert.ThrowsAsync(async () => + await fixture.RedmineManager.GetAsync(cancellationToken: cts.Token)); + } + + [Fact] + public async Task SendAsync_WhenWebExceptionOccurs_ThrowsRedmineApiException() + { + // Act & Assert + var exception = await Assert.ThrowsAnyAsync(async () => + await fixture.RedmineManager.GetAsync("xyz")); + + Assert.NotNull(exception.InnerException); + } + + [Fact] + public async Task SendAsync_WhenOperationCanceled_ThrowsRedmineOperationCanceledException() + { + using var cts = new CancellationTokenSource(); + // Arrange + await cts.CancelAsync(); + + // Act & Assert + _ = await Assert.ThrowsAsync(async () => + await fixture.RedmineManager.GetAsync(cancellationToken: cts.Token)); + } + + [Fact] + public async Task SendAsync_WhenOperationTimedOut_ThrowsRedmineOperationCanceledException() + { + // Arrange + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(1)); + + // Act & Assert + _ = await Assert.ThrowsAsync(async () => + await fixture.RedmineManager.GetAsync(cancellationToken: timeoutCts.Token)); + } + + [Fact] + public async Task SendAsync_WhenTaskCanceled_ThrowsRedmineOperationCanceledException() + { + using var cts = new CancellationTokenSource(); + // Arrange + cts.CancelAfter(TimeSpan.FromMilliseconds(50)); + + // Act & Assert + _ = await Assert.ThrowsAsync(async () => + await fixture.RedmineManager.GetAsync(cancellationToken: cts.Token)); + } + + [Fact] + public async Task SendAsync_WhenGeneralException_ThrowsRedmineException() + { + // Act & Assert + _ = await Assert.ThrowsAsync(async () => + await fixture.RedmineManager.CreateAsync(null)); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/appsettings.json b/tests/redmine-net-api.Integration.Tests/appsettings.json new file mode 100644 index 00000000..585ef671 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/appsettings.json @@ -0,0 +1,26 @@ +{ + "TestContainer": { + "Mode": "CreateNewWithRandomPorts", + "Redmine":{ + "Url": "$Url", + "Port": 3000, + "Image": "redmine:6.0.5-alpine", + "SqlFilePath": "TestData/init-redmine.sql", + "AuthenticationMode": "ApiKey", + "Authentication": { + "Basic":{ + "Username": "$Username", + "Password": "$Password" + }, + "ApiKey": "$ApiKey" + } + }, + "Postgres": { + "Port": 5432, + "Image": "postgres:17.4-alpine", + "Database": "postgres", + "User": "postgres", + "Password": "postgres" + } + } +} diff --git a/tests/redmine-net-api.Integration.Tests/appsettings.local.json b/tests/redmine-net-api.Integration.Tests/appsettings.local.json new file mode 100644 index 00000000..65fce933 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/appsettings.local.json @@ -0,0 +1,12 @@ +{ + "TestContainer": { + "Mode": "UseExisting", + "Redmine":{ + "Url": "http://localhost:8089/", + "AuthenticationMode": "ApiKey", + "Authentication": { + "ApiKey": "61d6fa45ca2c570372b08b8c54b921e5fc39335a" + } + } + } +} diff --git a/tests/redmine-net-api.Integration.Tests/redmine-net-api.Integration.Tests.csproj b/tests/redmine-net-api.Integration.Tests/redmine-net-api.Integration.Tests.csproj new file mode 100644 index 00000000..9f80bcd1 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/redmine-net-api.Integration.Tests.csproj @@ -0,0 +1,81 @@ + + + + |net40|net45|net451|net452|net46|net461| + |net45|net451|net452|net46|net461| + |net40|net45|net451|net452|net46|net461|net462|net470|net471|net472|net48|net481| + |net45|net451|net452|net46|net461|net462|net470|net471|net472|net48|net481| + + + + DEBUG;TRACE;DEBUG_XML + + + + DEBUG;TRACE;DEBUG_JSON + + + + net9.0 + redmine_net_api.Integration.Tests + enable + disable + false + Padi.DotNet.RedmineAPI.Integration.Tests + $(AssemblyName) + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/tests/redmine-net-api.Tests/.editorconfig b/tests/redmine-net-api.Tests/.editorconfig new file mode 100644 index 00000000..e45eade4 --- /dev/null +++ b/tests/redmine-net-api.Tests/.editorconfig @@ -0,0 +1,10 @@ +# To learn more about .editorconfig see https://aka.ms/editorconfigdocs +root = true + +# All files +[*] +indent_style = space + +# Xml files +[*.xml] +indent_size = 2 diff --git a/tests/redmine-net-api.Tests/Bugs/RedmineApi-229.cs b/tests/redmine-net-api.Tests/Bugs/RedmineApi-229.cs new file mode 100644 index 00000000..2c6bb2b8 --- /dev/null +++ b/tests/redmine-net-api.Tests/Bugs/RedmineApi-229.cs @@ -0,0 +1,124 @@ +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Bugs; + +public sealed class RedmineApi229 +{ + [Fact] + public void Equals_ShouldReturnTrue_WhenComparingWithSelf() + { + // Arrange + var timeEntry = CreateSampleTimeEntry(); + + // Act & Assert + Assert.True(timeEntry.Equals(timeEntry), "TimeEntry should equal itself (reference equality)"); + Assert.True(timeEntry == timeEntry, "TimeEntry should equal itself using == operator"); + Assert.True(timeEntry.Equals((object)timeEntry), "TimeEntry should equal itself when cast to object"); + Assert.Equal(timeEntry.GetHashCode(), timeEntry.GetHashCode()); + } + + [Fact] + public void Equals_ShouldReturnTrue_WhenComparingIdenticalInstances() + { + // Arrange + var timeEntry1 = CreateSampleTimeEntry(); + var timeEntry2 = CreateSampleTimeEntry(); + + // Act & Assert + Assert.True(timeEntry1.Equals(timeEntry2), "Identical TimeEntry instances should be equal"); + Assert.True(timeEntry2.Equals(timeEntry1), "Equality should be symmetric"); + Assert.Equal(timeEntry1.GetHashCode(), timeEntry2.GetHashCode()); + } + + [Fact] + public void Equals_ShouldReturnFalse_WhenComparingWithNull() + { + // Arrange + var timeEntry = CreateSampleTimeEntry(); + + // Act & Assert + Assert.False(timeEntry.Equals(null)); + Assert.False(timeEntry.Equals((object)null)); + } + + [Fact] + public void Equals_ShouldReturnFalse_WhenComparingDifferentTypes() + { + // Arrange + var timeEntry = CreateSampleTimeEntry(); + var differentObject = new object(); + + // Act & Assert + Assert.False(timeEntry.Equals(differentObject)); + } + + [Theory] + [MemberData(nameof(GetDifferentTimeEntries))] + public void Equals_ShouldReturnFalse_WhenPropertiesDiffer(TimeEntry different, string propertyName) + { + // Arrange + var baseline = CreateSampleTimeEntry(); + + // Act & Assert + Assert.False(baseline.Equals(different), $"TimeEntries should not be equal when {propertyName} differs"); + } + + private static TimeEntry CreateSampleTimeEntry() => new() + { + Id = 1, + Project = new IdentifiableName { Id = 1, Name = "Project" }, + Issue = new IdentifiableName { Id = 1, Name = "Issue" }, + User = new IdentifiableName { Id = 1, Name = "User" }, + Activity = new IdentifiableName { Id = 1, Name = "Activity" }, + Hours = (decimal)8.0, + Comments = "Test comment", + SpentOn = new DateTime(2023, 1, 1), + CreatedOn = new DateTime(2023, 1, 1), + UpdatedOn = new DateTime(2023, 1, 1), + CustomFields = + [ + new() { Id = 1, Name = "Field1"} + ] + }; + + public static TheoryData GetDifferentTimeEntries() + { + var data = new TheoryData(); + + // Different ID + var differentId = CreateSampleTimeEntry(); + differentId.Id = 2; + data.Add(differentId, "Id"); + + // Different Project + var differentProject = CreateSampleTimeEntry(); + differentProject.Project = new IdentifiableName { Id = 2, Name = "Different Project" }; + data.Add(differentProject, "Project"); + + // Different Issue + var differentIssue = CreateSampleTimeEntry(); + differentIssue.Issue = new IdentifiableName { Id = 2, Name = "Different Issue" }; + data.Add(differentIssue, "Issue"); + + // Different Hours + var differentHours = CreateSampleTimeEntry(); + differentHours.Hours = (decimal)4.0; + data.Add(differentHours, "Hours"); + + // Different CustomFields + var differentCustomFields = CreateSampleTimeEntry(); + differentCustomFields.CustomFields = + [ + new() { Id = 2, Name = "Field2" } + ]; + data.Add(differentCustomFields, "CustomFields"); + + // Different SpentOn + var differentSpentOn = CreateSampleTimeEntry(); + differentSpentOn.SpentOn = new DateTime(2023, 1, 2); + data.Add(differentSpentOn, "SpentOn"); + + return data; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs b/tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs new file mode 100644 index 00000000..2795f6c6 --- /dev/null +++ b/tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs @@ -0,0 +1,32 @@ +using System.Collections.Specialized; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Bugs; + +public sealed class RedmineApi371 : IClassFixture +{ + private readonly RedmineApiUrlsFixture _fixture; + + public RedmineApi371(RedmineApiUrlsFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public void Should_Return_IssueCategories_For_Project_Url() + { + var projectIdAsString = 1.ToInvariantString(); + var result = _fixture.Sut.GetListFragment( + new RequestOptions + { + QueryString = new NameValueCollection{ { RedmineKeys.PROJECT_ID, projectIdAsString } } + }); + + Assert.Equal($"projects/{projectIdAsString}/issue_categories.{_fixture.Format}", result); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Clone/AttachmentCloneTests.cs b/tests/redmine-net-api.Tests/Clone/AttachmentCloneTests.cs new file mode 100644 index 00000000..13990f54 --- /dev/null +++ b/tests/redmine-net-api.Tests/Clone/AttachmentCloneTests.cs @@ -0,0 +1,81 @@ +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Clone; + +public sealed class AttachmentCloneTests +{ + [Fact] + public void Clone_WithPopulatedProperties_ReturnsDeepCopy() + { + // Arrange + var attachment = new Attachment + { + Id = 1, + FileName = "test.txt", + FileSize = 1024, + ContentType = "text/plain", + Description = "Test file", + ContentUrl = "http://example.com/test.txt", + ThumbnailUrl = "http://example.com/thumb.txt", + Author = new IdentifiableName(1, "John Doe"), + CreatedOn = DateTime.Now + }; + + // Act + var clone = attachment.Clone(false); + + // Assert + Assert.NotNull(clone); + Assert.NotSame(attachment, clone); + Assert.Equal(attachment.Id, clone.Id); + Assert.Equal(attachment.FileName, clone.FileName); + Assert.Equal(attachment.FileSize, clone.FileSize); + Assert.Equal(attachment.ContentType, clone.ContentType); + Assert.Equal(attachment.Description, clone.Description); + Assert.Equal(attachment.ContentUrl, clone.ContentUrl); + Assert.Equal(attachment.ThumbnailUrl, clone.ThumbnailUrl); + Assert.Equal(attachment.CreatedOn, clone.CreatedOn); + + Assert.NotSame(attachment.Author, clone.Author); + Assert.Equal(attachment.Author.Id, clone.Author.Id); + Assert.Equal(attachment.Author.Name, clone.Author.Name); + } + + [Fact] + public void Clone_With_ResetId_True_Should_Return_A_Copy_With_Id_Set_Zero() + { + // Arrange + var attachment = new Attachment + { + Id = 1, + FileName = "test.txt", + FileSize = 1024, + ContentType = "text/plain", + Description = "Test file", + ContentUrl = "http://example.com/test.txt", + ThumbnailUrl = "http://example.com/thumb.txt", + Author = new IdentifiableName(1, "John Doe"), + CreatedOn = DateTime.Now + }; + + // Act + var clone = attachment.Clone(true); + + // Assert + Assert.NotNull(clone); + Assert.NotSame(attachment, clone); + Assert.NotEqual(attachment.Id, clone.Id); + Assert.Equal(attachment.FileName, clone.FileName); + Assert.Equal(attachment.FileSize, clone.FileSize); + Assert.Equal(attachment.ContentType, clone.ContentType); + Assert.Equal(attachment.Description, clone.Description); + Assert.Equal(attachment.ContentUrl, clone.ContentUrl); + Assert.Equal(attachment.ThumbnailUrl, clone.ThumbnailUrl); + Assert.Equal(attachment.CreatedOn, clone.CreatedOn); + + Assert.NotSame(attachment.Author, clone.Author); + Assert.Equal(attachment.Author.Id, clone.Author.Id); + Assert.Equal(attachment.Author.Name, clone.Author.Name); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Clone/IssueCloneTests.cs b/tests/redmine-net-api.Tests/Clone/IssueCloneTests.cs new file mode 100644 index 00000000..f41bcd2a --- /dev/null +++ b/tests/redmine-net-api.Tests/Clone/IssueCloneTests.cs @@ -0,0 +1,128 @@ +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Clone; + +public sealed class IssueCloneTests +{ + [Fact] + public void Clone_WithNullProperties_ReturnsNewInstanceWithNullProperties() + { + // Arrange + var issue = new Issue(); + + // Act + var clone = issue.Clone(true); + + // Assert + Assert.NotNull(clone); + Assert.NotSame(issue, clone); + Assert.Equal(issue.Id, clone.Id); + Assert.Null(clone.Project); + Assert.Null(clone.Tracker); + Assert.Null(clone.Status); + } + + [Fact] + public void Clone_WithPopulatedProperties_ReturnsDeepCopy() + { + // Arrange + var issue = CreateSampleIssue(); + + // Act + var clone = issue.Clone(true); + + // Assert + Assert.NotNull(clone); + Assert.NotSame(issue, clone); + + Assert.NotEqual(issue.Id, clone.Id); + Assert.Equal(issue.Subject, clone.Subject); + Assert.Equal(issue.Description, clone.Description); + Assert.Equal(issue.DoneRatio, clone.DoneRatio); + Assert.Equal(issue.IsPrivate, clone.IsPrivate); + Assert.Equal(issue.EstimatedHours, clone.EstimatedHours); + Assert.Equal(issue.CreatedOn, clone.CreatedOn); + Assert.Equal(issue.UpdatedOn, clone.UpdatedOn); + Assert.Equal(issue.ClosedOn, clone.ClosedOn); + + Assert.NotSame(issue.Project, clone.Project); + Assert.Equal(issue.Project.Id, clone.Project.Id); + Assert.Equal(issue.Project.Name, clone.Project.Name); + + Assert.NotSame(issue.Tracker, clone.Tracker); + Assert.Equal(issue.Tracker.Id, clone.Tracker.Id); + Assert.Equal(issue.Tracker.Name, clone.Tracker.Name); + + Assert.NotSame(issue.CustomFields, clone.CustomFields); + Assert.Equal(issue.CustomFields.Count, clone.CustomFields.Count); + for (var i = 0; i < issue.CustomFields.Count; i++) + { + Assert.NotSame(issue.CustomFields[i], clone.CustomFields[i]); + Assert.Equal(issue.CustomFields[i].Id, clone.CustomFields[i].Id); + Assert.Equal(issue.CustomFields[i].Name, clone.CustomFields[i].Name); + } + + Assert.NotNull(clone.Attachments); + Assert.Equal(issue.Attachments.Count, clone.Attachments.Count); + Assert.All(clone.Attachments, Assert.NotNull); + } + + [Fact] + public void Clone_ModifyingClone_DoesNotAffectOriginal() + { + // Arrange + var issue = CreateSampleIssue(); + var clone = issue.Clone(true); + + // Act + clone.Subject = "Modified Subject"; + clone.Project.Name = "Modified Project"; + clone.CustomFields[0].Values = [new CustomFieldValue("Modified Value")]; + + // Assert + Assert.NotEqual(issue.Subject, clone.Subject); + Assert.NotEqual(issue.Project.Name, clone.Project.Name); + Assert.NotEqual(issue.CustomFields[0].Values, clone.CustomFields[0].Values); + } + + private static Issue CreateSampleIssue() + { + return new Issue + { + Id = 1, + Project = new IdentifiableName(100, "Test Project"), + Tracker = new IdentifiableName(200, "Bug"), + Status = new IssueStatus(300, "New"), + Priority = new IdentifiableName(400, "Normal"), + Author = new IdentifiableName(500, "John Doe"), + Subject = "Test Issue", + Description = "Test Description", + StartDate = DateTime.Today, + DueDate = DateTime.Today.AddDays(7), + DoneRatio = 50, + IsPrivate = false, + EstimatedHours = 8.5f, + CreatedOn = DateTime.Now.AddDays(-1), + UpdatedOn = DateTime.Now, + CustomFields = + [ + new IssueCustomField + { + Id = 1, + Name = "Custom Field 1", + } + ], + Attachments = + [ + new Attachment + { + Id = 1, + FileName = "test.txt", + FileSize = 1024, + Author = new IdentifiableName(1, "Author") + } + ] + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Clone/JournalCloneTests.cs b/tests/redmine-net-api.Tests/Clone/JournalCloneTests.cs new file mode 100644 index 00000000..a6c11719 --- /dev/null +++ b/tests/redmine-net-api.Tests/Clone/JournalCloneTests.cs @@ -0,0 +1,58 @@ +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Clone; + +public sealed class JournalCloneTests +{ + [Fact] + public void Clone_WithPopulatedProperties_ReturnsDeepCopy() + { + // Arrange + var journal = new Journal + { + Id = 1, + User = new IdentifiableName(1, "John Doe"), + Notes = "Test notes", + CreatedOn = DateTime.Now, + PrivateNotes = true, + Details = (List) + [ + new Detail + { + Property = "status_id", + Name = "Status", + OldValue = "1", + NewValue = "2" + } + ] + }; + + // Act + var clone = journal.Clone(false); + + // Assert + Assert.NotNull(clone); + Assert.NotSame(journal, clone); + Assert.Equal(journal.Id, clone.Id); + Assert.Equal(journal.Notes, clone.Notes); + Assert.Equal(journal.CreatedOn, clone.CreatedOn); + Assert.Equal(journal.PrivateNotes, clone.PrivateNotes); + + Assert.NotSame(journal.User, clone.User); + Assert.Equal(journal.User.Id, clone.User.Id); + Assert.Equal(journal.User.Name, clone.User.Name); + + Assert.NotNull(clone.Details); + Assert.NotSame(journal.Details, clone.Details); + Assert.Equal(journal.Details.Count, clone.Details.Count); + + var originalDetail = journal.Details[0]; + var clonedDetail = clone.Details[0]; + Assert.NotSame(originalDetail, clonedDetail); + Assert.Equal(originalDetail.Property, clonedDetail.Property); + Assert.Equal(originalDetail.Name, clonedDetail.Name); + Assert.Equal(originalDetail.OldValue, clonedDetail.OldValue); + Assert.Equal(originalDetail.NewValue, clonedDetail.NewValue); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/AttachmentEqualityTests.cs b/tests/redmine-net-api.Tests/Equality/AttachmentEqualityTests.cs new file mode 100644 index 00000000..c604f699 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/AttachmentEqualityTests.cs @@ -0,0 +1,64 @@ +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class AttachmentEqualityTests +{ + [Fact] + public void Equals_SameReference_ReturnsTrue() + { + var attachment = CreateSampleAttachment(); + Assert.True(attachment.Equals(attachment)); + } + + [Fact] + public void Equals_Null_ReturnsFalse() + { + var attachment = CreateSampleAttachment(); + Assert.False(attachment.Equals(null)); + } + + [Theory] + [MemberData(nameof(GetDifferentAttachments))] + public void Equals_DifferentProperties_ReturnsFalse(Attachment attachment1, Attachment attachment2, string propertyName) + { + Assert.False(attachment1.Equals(attachment2), $"Attachments should not be equal when {propertyName} is different"); + } + + public static IEnumerable GetDifferentAttachments() + { + var baseAttachment = CreateSampleAttachment(); + + // Different FileName + var differentFileName = CreateSampleAttachment(); + differentFileName.FileName = "different.txt"; + yield return [baseAttachment, differentFileName, "FileName"]; + + // Different FileSize + var differentFileSize = CreateSampleAttachment(); + differentFileSize.FileSize = 2048; + yield return [baseAttachment, differentFileSize, "FileSize"]; + + // Different Author + var differentAuthor = CreateSampleAttachment(); + differentAuthor.Author = new IdentifiableName { Id = 999, Name = "Different Author" }; + yield return [baseAttachment, differentAuthor, "Author"]; + } + + private static Attachment CreateSampleAttachment() + { + return new Attachment + { + Id = 1, + FileName = "test.txt", + FileSize = 1024, + ContentType = "text/plain", + Description = "Test file", + ContentUrl = "https://example.com/test.txt", + ThumbnailUrl = "https://example.com/thumb.txt", + Author = new IdentifiableName { Id = 1, Name = "John Doe" }, + CreatedOn = DateTime.Now + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/BaseEqualityTests.cs b/tests/redmine-net-api.Tests/Equality/BaseEqualityTests.cs new file mode 100644 index 00000000..e6d8672f --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/BaseEqualityTests.cs @@ -0,0 +1,65 @@ +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public abstract class BaseEqualityTests where T : class, IEquatable + { + protected abstract T CreateSampleInstance(); + protected abstract T CreateDifferentInstance(); + + [Fact] + public void Equals_SameReference_ReturnsTrue() + { + var instance = CreateSampleInstance(); + Assert.True(instance.Equals(instance)); + } + + [Fact] + public void Equals_Null_ReturnsFalse() + { + var instance = CreateSampleInstance(); + Assert.False(instance.Equals(null)); + } + + [Fact] + public void Equals_DifferentType_ReturnsFalse() + { + var instance = CreateSampleInstance(); + var differentObject = new object(); + Assert.False(instance.Equals(differentObject)); + } + + [Fact] + public void Equals_IdenticalProperties_ReturnsTrue() + { + var instance1 = CreateSampleInstance(); + var instance2 = CreateSampleInstance(); + Assert.True(instance1.Equals(instance2)); + Assert.True(instance2.Equals(instance1)); + } + + [Fact] + public void Equals_DifferentProperties_ReturnsFalse() + { + var instance1 = CreateSampleInstance(); + var instance2 = CreateDifferentInstance(); + Assert.False(instance1.Equals(instance2)); + Assert.False(instance2.Equals(instance1)); + } + + [Fact] + public void GetHashCode_SameProperties_ReturnsSameValue() + { + var instance1 = CreateSampleInstance(); + var instance2 = CreateSampleInstance(); + Assert.Equal(instance1.GetHashCode(), instance2.GetHashCode()); + } + + [Fact] + public void GetHashCode_DifferentProperties_ReturnsDifferentValues() + { + var instance1 = CreateSampleInstance(); + var instance2 = CreateDifferentInstance(); + Assert.NotEqual(instance1.GetHashCode(), instance2.GetHashCode()); + } + } \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/CustomFieldPossibleValueTests.cs b/tests/redmine-net-api.Tests/Equality/CustomFieldPossibleValueTests.cs new file mode 100644 index 00000000..c6d0fdfc --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/CustomFieldPossibleValueTests.cs @@ -0,0 +1,24 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class CustomFieldPossibleValueTests : BaseEqualityTests +{ + protected override CustomFieldPossibleValue CreateSampleInstance() + { + return new CustomFieldPossibleValue + { + Value = "test-value", + Label = "Test Label" + }; + } + + protected override CustomFieldPossibleValue CreateDifferentInstance() + { + return new CustomFieldPossibleValue + { + Value = "different-value", + Label = "Different Label" + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/CustomFieldRoleTests.cs b/tests/redmine-net-api.Tests/Equality/CustomFieldRoleTests.cs new file mode 100644 index 00000000..7026ff0c --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/CustomFieldRoleTests.cs @@ -0,0 +1,24 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class CustomFieldRoleTests : BaseEqualityTests +{ + protected override IdentifiableName CreateSampleInstance() + { + return new CustomFieldRole + { + Id = 1, + Name = "Test Role" + }; + } + + protected override IdentifiableName CreateDifferentInstance() + { + return new CustomFieldRole + { + Id = 2, + Name = "Different Role" + }; + } +} diff --git a/tests/redmine-net-api.Tests/Equality/CustomFieldTests.cs b/tests/redmine-net-api.Tests/Equality/CustomFieldTests.cs new file mode 100644 index 00000000..4ae461b5 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/CustomFieldTests.cs @@ -0,0 +1,34 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class CustomFieldTests : BaseEqualityTests +{ + protected override CustomField CreateSampleInstance() + { + return new CustomField + { + Id = 1, + Name = "Test Field", + CustomizedType = "issue", + FieldFormat = "string", + Regexp = "", + MinLength = 0, + MaxLength = 100, + IsRequired = false, + IsFilter = true, + Searchable = true, + Multiple = false, + DefaultValue = "default", + Visible = true, + PossibleValues = [new CustomFieldPossibleValue { Value = "value1", Label = "Label 1" }] + }; + } + + protected override CustomField CreateDifferentInstance() + { + var field = CreateSampleInstance(); + field.Name = "Different Field"; + return field; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/DetailTests.cs b/tests/redmine-net-api.Tests/Equality/DetailTests.cs new file mode 100644 index 00000000..296a21d3 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/DetailTests.cs @@ -0,0 +1,28 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class DetailTests : BaseEqualityTests +{ + protected override Detail CreateSampleInstance() + { + return new Detail + { + Property = "status", + Name = "Status", + OldValue = "1", + NewValue = "2" + }; + } + + protected override Detail CreateDifferentInstance() + { + return new Detail + { + Property = "priority", + Name = "Priority", + OldValue = "3", + NewValue = "4" + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/ErrorTests.cs b/tests/redmine-net-api.Tests/Equality/ErrorTests.cs new file mode 100644 index 00000000..46f52c3b --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/ErrorTests.cs @@ -0,0 +1,16 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class ErrorTests : BaseEqualityTests +{ + protected override Error CreateSampleInstance() + { + return new Error( "Test error" ); + } + + protected override Error CreateDifferentInstance() + { + return new Error("Different error"); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/GroupTests.cs b/tests/redmine-net-api.Tests/Equality/GroupTests.cs new file mode 100644 index 00000000..ee272db1 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/GroupTests.cs @@ -0,0 +1,26 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class GroupTests : BaseEqualityTests +{ + protected override Group CreateSampleInstance() + { + return new Group + { + Id = 1, + Name = "Test Group", + Users = [new GroupUser { Id = 1, Name = "User 1" }], + CustomFields = [new IssueCustomField { Id = 1, Name = "Field 1" }], + Memberships = [new Membership { Id = 1, Project = new IdentifiableName { Id = 1, Name = "Project 1" } }] + }; + } + + protected override Group CreateDifferentInstance() + { + var group = CreateSampleInstance(); + group.Name = "Different Group"; + group.Users = [new GroupUser { Id = 2, Name = "User 2" }]; + return group; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/GroupUserTests.cs b/tests/redmine-net-api.Tests/Equality/GroupUserTests.cs new file mode 100644 index 00000000..f177b4eb --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/GroupUserTests.cs @@ -0,0 +1,24 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class GroupUserTests : BaseEqualityTests +{ + protected override IdentifiableName CreateSampleInstance() + { + return new GroupUser + { + Id = 1, + Name = "Test User" + }; + } + + protected override IdentifiableName CreateDifferentInstance() + { + return new GroupUser + { + Id = 2, + Name = "Different User" + }; + } +} diff --git a/tests/redmine-net-api.Tests/Equality/IssueCategoryTests.cs b/tests/redmine-net-api.Tests/Equality/IssueCategoryTests.cs new file mode 100644 index 00000000..1a00e021 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/IssueCategoryTests.cs @@ -0,0 +1,28 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class IssueCategoryTests : BaseEqualityTests +{ + protected override IssueCategory CreateSampleInstance() + { + return new IssueCategory + { + Id = 1, + Name = "Test Category", + Project = new IdentifiableName { Id = 1, Name = "Project 1" }, + AssignTo = new IdentifiableName { Id = 1, Name = "User 1" } + }; + } + + protected override IssueCategory CreateDifferentInstance() + { + return new IssueCategory + { + Id = 2, + Name = "Different Category", + Project = new IdentifiableName { Id = 2, Name = "Project 2" }, + AssignTo = new IdentifiableName { Id = 2, Name = "User 2" } + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/IssueEqualityTests.cs b/tests/redmine-net-api.Tests/Equality/IssueEqualityTests.cs new file mode 100644 index 00000000..2d137336 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/IssueEqualityTests.cs @@ -0,0 +1,113 @@ +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class IssueEqualityTests +{ + [Fact] + public void Equals_SameReference_ReturnsTrue() + { + var issue = CreateSampleIssue(); + Assert.True(issue.Equals(issue)); + } + + [Fact] + public void Equals_Null_ReturnsFalse() + { + var issue = CreateSampleIssue(); + Assert.False(issue.Equals(null)); + } + + [Fact] + public void Equals_DifferentType_ReturnsFalse() + { + var issue = CreateSampleIssue(); + var differentObject = new object(); + Assert.False(issue.Equals(differentObject)); + } + + [Fact] + public void Equals_IdenticalProperties_ReturnsTrue() + { + var issue1 = CreateSampleIssue(); + var issue2 = CreateSampleIssue(); + Assert.True(issue1.Equals(issue2)); + Assert.True(issue2.Equals(issue1)); + } + + [Fact] + public void GetHashCode_SameProperties_ReturnsSameValue() + { + var issue1 = CreateSampleIssue(); + var issue2 = CreateSampleIssue(); + Assert.Equal(issue1.GetHashCode(), issue2.GetHashCode()); + } + + [Fact] + public void OperatorEquals_SameObjects_ReturnsTrue() + { + var issue1 = CreateSampleIssue(); + var issue2 = CreateSampleIssue(); + Assert.True(issue1 == issue2); + } + + [Fact] + public void OperatorNotEquals_DifferentObjects_ReturnsTrue() + { + var issue1 = CreateSampleIssue(); + var issue2 = CreateSampleIssue(); + issue2.Subject = "Different Subject"; + Assert.True(issue1 != issue2); + } + + [Fact] + public void Equals_NullCollections_ReturnsTrue() + { + var issue1 = CreateSampleIssue(); + var issue2 = CreateSampleIssue(); + issue1.CustomFields = null; + issue2.CustomFields = null; + Assert.True(issue1.Equals(issue2)); + } + + [Fact] + public void Equals_DifferentCollectionSizes_ReturnsFalse() + { + var issue1 = CreateSampleIssue(); + var issue2 = CreateSampleIssue(); + issue2.CustomFields.Add(new IssueCustomField { Id = 2, Name = "Additional Field" }); + Assert.False(issue1.Equals(issue2)); + } + + private static Issue CreateSampleIssue() + { + return new Issue + { + Id = 1, + Project = new IdentifiableName { Id = 100, Name = "Test Project" }, + Tracker = new IdentifiableName { Id = 1, Name = "Bug" }, + Status = new IssueStatus { Id = 1, Name = "New" }, + Priority = new IdentifiableName { Id = 1, Name = "Normal" }, + Author = new IdentifiableName { Id = 1, Name = "John Doe" }, + Subject = "Test Issue", + Description = "Test Description", + StartDate = new DateTime(2025, 02,02,10,10,10).Date, + DueDate = new DateTime(2025, 02,02,10,10,10).Date.AddDays(7), + DoneRatio = 0, + IsPrivate = false, + EstimatedHours = 8.5f, + CreatedOn = new DateTime(2025, 02,02,10,10,10), + UpdatedOn = new DateTime(2025, 02,04,15,10,5), + CustomFields = + [ + new IssueCustomField + { + Id = 1, + Name = "Custom Field 1", + Values = [new CustomFieldValue("Value 1")] + } + ] + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/IssueStatusTests.cs b/tests/redmine-net-api.Tests/Equality/IssueStatusTests.cs new file mode 100644 index 00000000..793f7e5c --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/IssueStatusTests.cs @@ -0,0 +1,28 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class IssueStatusTests : BaseEqualityTests +{ + protected override IssueStatus CreateSampleInstance() + { + return new IssueStatus + { + Id = 1, + Name = "New", + IsDefault = true, + IsClosed = false + }; + } + + protected override IssueStatus CreateDifferentInstance() + { + return new IssueStatus + { + Id = 2, + Name = "Closed", + IsDefault = false, + IsClosed = true + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/JournalEqualityTests.cs b/tests/redmine-net-api.Tests/Equality/JournalEqualityTests.cs new file mode 100644 index 00000000..c0ea67d4 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/JournalEqualityTests.cs @@ -0,0 +1,70 @@ +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class JournalEqualityTests +{ + [Fact] + public void Equals_SameReference_ReturnsTrue() + { + var journal = CreateSampleJournal(); + Assert.True(journal.Equals(journal)); + } + + [Fact] + public void Equals_Null_ReturnsFalse() + { + var journal = CreateSampleJournal(); + Assert.False(journal.Equals(null)); + } + + [Theory] + [MemberData(nameof(GetDifferentJournals))] + public void Equals_DifferentProperties_ReturnsFalse(Journal journal1, Journal journal2, string propertyName) + { + Assert.False(journal1.Equals(journal2), $"Journals should not be equal when {propertyName} is different"); + } + + public static IEnumerable GetDifferentJournals() + { + var baseJournal = CreateSampleJournal(); + + // Different Notes + var differentNotes = CreateSampleJournal(); + differentNotes.Notes = "Different notes"; + yield return [baseJournal, differentNotes, "Notes"]; + + // Different User + var differentUser = CreateSampleJournal(); + differentUser.User = new IdentifiableName { Id = 999, Name = "Different User" }; + yield return [baseJournal, differentUser, "User"]; + + // Different Details + var differentDetails = CreateSampleJournal(); + differentDetails.Details[0].NewValue = "Different value"; + yield return [baseJournal, differentDetails, "Details"]; + } + + private static Journal CreateSampleJournal() + { + return new Journal + { + Id = 1, + User = new IdentifiableName { Id = 1, Name = "John Doe" }, + Notes = "Test notes", + CreatedOn = new DateTime(2025,02,14,14,04,00), + PrivateNotes = true, + Details = + [ + new Detail + { + Property = "status_id", + Name = "Status", + OldValue = "1", + NewValue = "2" + } + ] + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/MembershipTests.cs b/tests/redmine-net-api.Tests/Equality/MembershipTests.cs new file mode 100644 index 00000000..41f15546 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/MembershipTests.cs @@ -0,0 +1,28 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class MembershipTests : BaseEqualityTests +{ + protected override Membership CreateSampleInstance() + { + return new Membership + { + Id = 1, + Project = new IdentifiableName { Id = 1, Name = "Project 1" }, + User = new IdentifiableName { Id = 1, Name = "User 1" }, + Roles = [new MembershipRole { Id = 1, Name = "Developer", Inherited = false }] + }; + } + + protected override Membership CreateDifferentInstance() + { + return new Membership + { + Id = 2, + Project = new IdentifiableName { Id = 2, Name = "Project 2" }, + User = new IdentifiableName { Id = 2, Name = "User 2" }, + Roles = [new MembershipRole { Id = 2, Name = "Manager", Inherited = true }] + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/MyAccountCustomFieldTests.cs b/tests/redmine-net-api.Tests/Equality/MyAccountCustomFieldTests.cs new file mode 100644 index 00000000..aa2036d6 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/MyAccountCustomFieldTests.cs @@ -0,0 +1,26 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public class MyAccountCustomFieldTests : BaseEqualityTests +{ + protected override MyAccountCustomField CreateSampleInstance() + { + return new MyAccountCustomField + { + Id = 1, + Name = "Test Field", + Value = "Test Value", + }; + } + + protected override MyAccountCustomField CreateDifferentInstance() + { + return new MyAccountCustomField + { + Id = 2, + Name = "Different Field", + Value = "Different Value", + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/MyAccountTests.cs b/tests/redmine-net-api.Tests/Equality/MyAccountTests.cs new file mode 100644 index 00000000..f098f5f2 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/MyAccountTests.cs @@ -0,0 +1,39 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class MyAccountTests : BaseEqualityTests +{ + protected override MyAccount CreateSampleInstance() + { + return new MyAccount + { + Id = 1, + Login = "testaccount", + FirstName = "Test", + LastName = "Account", + Email = "test@example.com", + CreatedOn = new DateTime(2023, 1, 1).Date, + LastLoginOn = new DateTime(2023, 1, 1).Date, + ApiKey = "abc123", + CustomFields = [ + new MyAccountCustomField() { Value = "Value 1" } + ] + }; + } + + protected override MyAccount CreateDifferentInstance() + { + return new MyAccount + { + Id = 2, + Login = "differentaccount", + FirstName = "Different", + LastName = "Account", + Email = "different@example.com", + CreatedOn = new DateTime(2023, 1, 2).Date, + LastLoginOn = new DateTime(2023, 1, 2).Date, + ApiKey = "xyz789" + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/NewsTests.cs b/tests/redmine-net-api.Tests/Equality/NewsTests.cs new file mode 100644 index 00000000..0851518c --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/NewsTests.cs @@ -0,0 +1,36 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class NewsTests : BaseEqualityTests +{ + protected override News CreateSampleInstance() + { + return new News + { + Id = 1, + Project = new IdentifiableName { Id = 1, Name = "Project 1" }, + Author = new IdentifiableName { Id = 1, Name = "Author 1" }, + Title = "Test News", + Summary = "Test Summary", + Description = "Test Description", + CreatedOn = new DateTime(2023, 1, 1, 0, 0, 0).Date, + Comments = [new NewsComment { Id = 1, Content = "Test Comment" }] + }; + } + + protected override News CreateDifferentInstance() + { + return new News + { + Id = 2, + Project = new IdentifiableName { Id = 2, Name = "Project 2" }, + Author = new IdentifiableName { Id = 2, Name = "Author 2" }, + Title = "Different News", + Summary = "Different Summary", + Description = "Different Description", + CreatedOn = new DateTime(2023, 1, 2).Date, + Comments = [new NewsComment { Id = 2, Content = "Different Comment" }] + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/PermissionTests.cs b/tests/redmine-net-api.Tests/Equality/PermissionTests.cs new file mode 100644 index 00000000..cc35641b --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/PermissionTests.cs @@ -0,0 +1,22 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class PermissionTests : BaseEqualityTests +{ + protected override Permission CreateSampleInstance() + { + return new Permission + { + Info = "add_issues" + }; + } + + protected override Permission CreateDifferentInstance() + { + return new Permission + { + Info = "edit_issues" + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/ProjectMembershipTests.cs b/tests/redmine-net-api.Tests/Equality/ProjectMembershipTests.cs new file mode 100644 index 00000000..6ed4ea0d --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/ProjectMembershipTests.cs @@ -0,0 +1,28 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class ProjectMembershipTests : BaseEqualityTests +{ + protected override ProjectMembership CreateSampleInstance() + { + return new ProjectMembership + { + Id = 1, + Project = new IdentifiableName { Id = 1, Name = "Project 1" }, + User = new IdentifiableName { Id = 1, Name = "User 1" }, + Roles = [new MembershipRole { Id = 1, Name = "Developer" }] + }; + } + + protected override ProjectMembership CreateDifferentInstance() + { + return new ProjectMembership + { + Id = 2, + Project = new IdentifiableName { Id = 2, Name = "Project 2" }, + User = new IdentifiableName { Id = 2, Name = "User 2" }, + Roles = [new MembershipRole { Id = 2, Name = "Manager" }] + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/ProjectTests.cs b/tests/redmine-net-api.Tests/Equality/ProjectTests.cs new file mode 100644 index 00000000..c09cb3c6 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/ProjectTests.cs @@ -0,0 +1,92 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class ProjectTests : BaseEqualityTests +{ + protected override Project CreateSampleInstance() + { + return new Project + { + Id = 1, + Name = "Test Project", + Identifier = "test-project", + Description = "Test Description", + HomePage = "https://test.com/", + Status = ProjectStatus.Active, + IsPublic = true, + InheritMembers = true, + DefaultAssignee = new IdentifiableName(5, "DefaultAssignee"), + DefaultVersion = new IdentifiableName(5, "DefaultVersion"), + Parent = new IdentifiableName { Id = 1, Name = "Parent Project" }, + CreatedOn = new DateTime(2023, 1, 1).Date, + UpdatedOn = new DateTime(2023, 1, 1).Date, + Trackers = + [ + new() { Id = 1, Name = "Bug" }, + new() { Id = 2, Name = "Feature" } + ], + + CustomFieldValues = + [ + new() { Id = 1, Name = "Field1"}, + new() { Id = 2, Name = "Field2"} + ], + + IssueCategories = + [ + new() { Id = 1, Name = "Category1" }, + new() { Id = 2, Name = "Category2" } + ], + EnabledModules = + [ + new() { Id = 1, Name = "Module1" }, + new() { Id = 2, Name = "Module2" } + ], + TimeEntryActivities = + [ + new() { Id = 1, Name = "Activity1" }, + new() { Id = 2, Name = "Activity2" } + ], + IssueCustomFields = [IssueCustomField.CreateSingle(1, "SingleCustomField", "SingleCustomFieldValue")] + }; + } + + protected override Project CreateDifferentInstance() + { + return new Project + { + Id = 2, + Name = "Different Project", + Identifier = "different-project", + Description = "Different Description", + HomePage = "https://different.com/", + Status = ProjectStatus.Archived, + IsPublic = false, + Parent = new IdentifiableName { Id = 2, Name = "Different Parent" }, + CreatedOn = new DateTime(2023, 1, 2).Date, + UpdatedOn = new DateTime(2023, 1, 2).Date, + Trackers = + [ + new() { Id = 3, Name = "Different Bug" } + ], + CustomFieldValues = + [ + new() { Id = 3, Name = "DifferentField"} + ], + IssueCategories = + [ + new() { Id = 3, Name = "DifferentCategory" } + ], + EnabledModules = + [ + new() { Id = 3, Name = "DifferentModule" } + ], + TimeEntryActivities = + [ + new() { Id = 3, Name = "DifferentActivity" } + ], + IssueCustomFields = [IssueCustomField.CreateSingle(1, "DifferentSingleCustomField", "DifferentSingleCustomFieldValue")] + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/QueryTests.cs b/tests/redmine-net-api.Tests/Equality/QueryTests.cs new file mode 100644 index 00000000..74b8beee --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/QueryTests.cs @@ -0,0 +1,28 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class QueryTests : BaseEqualityTests +{ + protected override Query CreateSampleInstance() + { + return new Query + { + Id = 1, + Name = "Test Query", + IsPublic = true, + ProjectId = 1 + }; + } + + protected override Query CreateDifferentInstance() + { + return new Query + { + Id = 2, + Name = "Different Query", + IsPublic = false, + ProjectId = 2 + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/RoleTests.cs b/tests/redmine-net-api.Tests/Equality/RoleTests.cs new file mode 100644 index 00000000..8879c4ba --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/RoleTests.cs @@ -0,0 +1,35 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class RoleTests : BaseEqualityTests +{ + protected override Role CreateSampleInstance() + { + return new Role + { + Id = 1, + Name = "Developer", + Permissions = + [ + new Permission { Info = "add_issues" }, + new Permission { Info = "edit_issues" } + ], + IsAssignable = true + }; + } + + protected override Role CreateDifferentInstance() + { + return new Role + { + Id = 2, + Name = "Manager", + Permissions = + [ + new Permission { Info = "manage_project" } + ], + IsAssignable = false + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/SearchTests.cs b/tests/redmine-net-api.Tests/Equality/SearchTests.cs new file mode 100644 index 00000000..2f5f0707 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/SearchTests.cs @@ -0,0 +1,32 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class SearchTests : BaseEqualityTests +{ + protected override Search CreateSampleInstance() + { + return new Search + { + Id = 1, + Title = "Test Search", + Type = "issue", + Url = "http://example.com/search", + Description = "Test Description", + DateTime = new DateTime(2023, 1, 1).Date + }; + } + + protected override Search CreateDifferentInstance() + { + return new Search + { + Id = 2, + Title = "Different Search", + Type = "wiki", + Url = "http://example.com/different", + Description = "Different Description", + DateTime = new DateTime(2023, 1, 2).Date + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/TimeEntryActivityTests.cs b/tests/redmine-net-api.Tests/Equality/TimeEntryActivityTests.cs new file mode 100644 index 00000000..a1554384 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/TimeEntryActivityTests.cs @@ -0,0 +1,28 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class TimeEntryActivityTests : BaseEqualityTests +{ + protected override TimeEntryActivity CreateSampleInstance() + { + return new TimeEntryActivity + { + Id = 1, + Name = "Development", + IsDefault = true, + IsActive = true + }; + } + + protected override TimeEntryActivity CreateDifferentInstance() + { + return new TimeEntryActivity + { + Id = 2, + Name = "Testing", + IsDefault = false, + IsActive = false + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/TimeEntryTests.cs b/tests/redmine-net-api.Tests/Equality/TimeEntryTests.cs new file mode 100644 index 00000000..445358ff --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/TimeEntryTests.cs @@ -0,0 +1,52 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class TimeEntryTests : BaseEqualityTests +{ + protected override TimeEntry CreateSampleInstance() + { + return new TimeEntry + { + Id = 1, + Project = new IdentifiableName { Id = 1, Name = "Project 1" }, + Issue = new IdentifiableName { Id = 1, Name = "Issue 1" }, + User = new IdentifiableName { Id = 1, Name = "User 1" }, + Activity = new IdentifiableName { Id = 1, Name = "Development" }, + Hours = (decimal)8.0, + Comments = "Work done", + SpentOn = new DateTime(2023, 1, 1).Date, + CreatedOn = new DateTime(2023, 1, 1).Date, + UpdatedOn = new DateTime(2023, 1, 1).Date, + CustomFields = + [ + new IssueCustomField + { + Id = 1, + Name = "Field 1", + Values = + [ + new CustomFieldValue("value") + ] + } + ] + }; + } + + protected override TimeEntry CreateDifferentInstance() + { + return new TimeEntry + { + Id = 2, + Project = new IdentifiableName { Id = 2, Name = "Project 2" }, + Issue = new IdentifiableName { Id = 2, Name = "Issue 2" }, + User = new IdentifiableName { Id = 2, Name = "User 2" }, + Activity = new IdentifiableName { Id = 2, Name = "Testing" }, + Hours = (decimal)4.0, + Comments = "Different work", + SpentOn = new DateTime(2023, 1, 2).Date, + CreatedOn = new DateTime(2023, 1, 2).Date, + UpdatedOn = new DateTime(2023, 1, 2).Date + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/TrackerCoreFieldTests.cs b/tests/redmine-net-api.Tests/Equality/TrackerCoreFieldTests.cs new file mode 100644 index 00000000..24f6cecb --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/TrackerCoreFieldTests.cs @@ -0,0 +1,16 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class TrackerCoreFieldTests : BaseEqualityTests +{ + protected override TrackerCoreField CreateSampleInstance() + { + return new TrackerCoreField("Developer"); + } + + protected override TrackerCoreField CreateDifferentInstance() + { + return new TrackerCoreField("Admin"); + } +} diff --git a/tests/redmine-net-api.Tests/Equality/TrackerCustomFieldTests.cs b/tests/redmine-net-api.Tests/Equality/TrackerCustomFieldTests.cs new file mode 100644 index 00000000..e06fd6ea --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/TrackerCustomFieldTests.cs @@ -0,0 +1,24 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class TrackerCustomFieldTests : BaseEqualityTests +{ + protected override IdentifiableName CreateSampleInstance() + { + return new TrackerCustomField + { + Id = 1, + Name = "Test Field" + }; + } + + protected override IdentifiableName CreateDifferentInstance() + { + return new TrackerCustomField + { + Id = 2, + Name = "Different Field" + }; + } +} diff --git a/tests/redmine-net-api.Tests/Equality/UploadTests.cs b/tests/redmine-net-api.Tests/Equality/UploadTests.cs new file mode 100644 index 00000000..48ded660 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/UploadTests.cs @@ -0,0 +1,28 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class UploadTests : BaseEqualityTests +{ + protected override Upload CreateSampleInstance() + { + return new Upload + { + Token = "abc123", + FileName = "test.pdf", + ContentType = "application/pdf", + Description = "Test Upload" + }; + } + + protected override Upload CreateDifferentInstance() + { + return new Upload + { + Token = "xyz789", + FileName = "different.pdf", + ContentType = "application/pdf", + Description = "Different Upload" + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/UserGroupTests.cs b/tests/redmine-net-api.Tests/Equality/UserGroupTests.cs new file mode 100644 index 00000000..005809cb --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/UserGroupTests.cs @@ -0,0 +1,25 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class UserGroupTests : BaseEqualityTests +{ + protected override IdentifiableName CreateSampleInstance() + { + return new UserGroup + { + Id = 1, + Name = "Test Group" + }; + } + + protected override IdentifiableName CreateDifferentInstance() + { + return new UserGroup + { + Id = 2, + Name = "Different Group" + }; + } +} + diff --git a/tests/redmine-net-api.Tests/Equality/UserTests.cs b/tests/redmine-net-api.Tests/Equality/UserTests.cs new file mode 100644 index 00000000..018163ec --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/UserTests.cs @@ -0,0 +1,64 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class UserTests : BaseEqualityTests +{ + protected override User CreateSampleInstance() + { + return new User + { + Id = 1, + Login = "testuser", + FirstName = "Test", + LastName = "User", + Email = "test@example.com", + CreatedOn = new DateTime(2023, 1, 1).Date, + LastLoginOn = new DateTime(2023, 1, 1).Date, + ApiKey = "abc123", + Status = UserStatus.StatusActive, + IsAdmin = false, + CustomFields = + [ + new IssueCustomField + { + Id = 1, + Name = "Field 1", + Values = + [ + new CustomFieldValue("Value 1") + ] + } + ], + Memberships = + [ + new Membership + { + Id = 1, + Project = new IdentifiableName { Id = 1, Name = "Project 1" } + } + ], + Groups = + [ + new UserGroup { Id = 1, Name = "Group 1" } + ] + }; + } + + protected override User CreateDifferentInstance() + { + return new User + { + Id = 2, + Login = "differentuser", + FirstName = "Different", + LastName = "User", + Email = "different@example.com", + CreatedOn = new DateTime(2023, 1, 2).Date, + LastLoginOn = new DateTime(2023, 1, 2).Date, + ApiKey = "xyz789", + Status = UserStatus.StatusLocked, + IsAdmin = true + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/VersionTests.cs b/tests/redmine-net-api.Tests/Equality/VersionTests.cs new file mode 100644 index 00000000..786d82b8 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/VersionTests.cs @@ -0,0 +1,46 @@ +using Redmine.Net.Api.Types; +using Version = Redmine.Net.Api.Types.Version; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class VersionTests : BaseEqualityTests +{ + protected override Version CreateSampleInstance() + { + return new Version + { + Id = 1, + Project = new IdentifiableName { Id = 1, Name = "Project 1" }, + Name = "1.0.0", + Description = "First Release", + Status = VersionStatus.Open, + DueDate = new DateTime(2023, 12, 31).Date, + CreatedOn = new DateTime(2023, 1, 1).Date, + UpdatedOn = new DateTime(2023, 1, 1).Date, + Sharing = VersionSharing.None, + CustomFields = + [ + new IssueCustomField + { + Id = 1, Name = "Field 1", Values = [new CustomFieldValue("Value 1")] + } + ] + }; + } + + protected override Version CreateDifferentInstance() + { + return new Version + { + Id = 2, + Project = new IdentifiableName { Id = 2, Name = "Project 2" }, + Name = "2.0.0", + Description = "Second Release", + Status = VersionStatus.Closed, + DueDate = new DateTime(2024, 12, 31).Date, + CreatedOn = new DateTime(2023, 1, 2).Date, + UpdatedOn = new DateTime(2023, 1, 2).Date, + Sharing = VersionSharing.System + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/WatcherTests.cs b/tests/redmine-net-api.Tests/Equality/WatcherTests.cs new file mode 100644 index 00000000..300ece6f --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/WatcherTests.cs @@ -0,0 +1,22 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class WatcherTests : BaseEqualityTests +{ + protected override Watcher CreateSampleInstance() + { + return new Watcher + { + Id = 1, + }; + } + + protected override Watcher CreateDifferentInstance() + { + return new Watcher + { + Id = 2, + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/WikiPageTests.cs b/tests/redmine-net-api.Tests/Equality/WikiPageTests.cs new file mode 100644 index 00000000..39284d57 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/WikiPageTests.cs @@ -0,0 +1,46 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class WikiPageTests : BaseEqualityTests +{ + protected override WikiPage CreateSampleInstance() + { + return new WikiPage + { + Id = 1, + Title = "Home Page", + Text = "Welcome to the wiki", + Version = 1, + Author = new IdentifiableName { Id = 1, Name = "Author 1" }, + Comments = "Initial version", + CreatedOn = new DateTime(2023, 1, 1), + UpdatedOn = new DateTime(2023, 1, 1), + Attachments = + [ + new Attachment + { + Id = 1, + FileName = "doc.pdf", + FileSize = 1024, + Author = new IdentifiableName { Id = 1, Name = "Author 1" } + } + ] + }; + } + + protected override WikiPage CreateDifferentInstance() + { + return new WikiPage + { + Id = 2, + Title = "Different Page", + Text = "Different content", + Version = 2, + Author = new IdentifiableName { Id = 2, Name = "Author 2" }, + Comments = "Updated version", + CreatedOn = new DateTime(2023, 1, 2), + UpdatedOn = new DateTime(2023, 1, 2) + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Infrastructure/Collections/JsonRedmineSerializerCollection.cs b/tests/redmine-net-api.Tests/Infrastructure/Collections/JsonRedmineSerializerCollection.cs new file mode 100644 index 00000000..45b0fe86 --- /dev/null +++ b/tests/redmine-net-api.Tests/Infrastructure/Collections/JsonRedmineSerializerCollection.cs @@ -0,0 +1,7 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure.Collections; + +[CollectionDefinition(Constants.JsonRedmineSerializerCollection)] +public sealed class JsonRedmineSerializerCollection : ICollectionFixture { } \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Infrastructure/Collections/XmlRedmineSerializerCollection.cs b/tests/redmine-net-api.Tests/Infrastructure/Collections/XmlRedmineSerializerCollection.cs new file mode 100644 index 00000000..02ca7492 --- /dev/null +++ b/tests/redmine-net-api.Tests/Infrastructure/Collections/XmlRedmineSerializerCollection.cs @@ -0,0 +1,7 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure.Collections; + +[CollectionDefinition(Constants.XmlRedmineSerializerCollection)] +public sealed class XmlRedmineSerializerCollection : ICollectionFixture { } \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Infrastructure/Constants.cs b/tests/redmine-net-api.Tests/Infrastructure/Constants.cs new file mode 100644 index 00000000..f680e785 --- /dev/null +++ b/tests/redmine-net-api.Tests/Infrastructure/Constants.cs @@ -0,0 +1,8 @@ +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure; + +public static class Constants +{ + public const string XmlRedmineSerializerCollection = "XmlRedmineSerializerCollection"; + public const string JsonRedmineSerializerCollection = "JsonRedmineSerializerCollection"; + public const string RedmineCollection = "RedmineCollection"; +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Infrastructure/Fixtures/JsonSerializerFixture.cs b/tests/redmine-net-api.Tests/Infrastructure/Fixtures/JsonSerializerFixture.cs new file mode 100644 index 00000000..35c40898 --- /dev/null +++ b/tests/redmine-net-api.Tests/Infrastructure/Fixtures/JsonSerializerFixture.cs @@ -0,0 +1,10 @@ +using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; + +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; + +public sealed class JsonSerializerFixture +{ + internal IRedmineSerializer Serializer { get; private set; } = new JsonRedmineSerializer(); + +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Infrastructure/Fixtures/RedmineApiUrlsFixture.cs b/tests/redmine-net-api.Tests/Infrastructure/Fixtures/RedmineApiUrlsFixture.cs new file mode 100644 index 00000000..a19b44ac --- /dev/null +++ b/tests/redmine-net-api.Tests/Infrastructure/Fixtures/RedmineApiUrlsFixture.cs @@ -0,0 +1,31 @@ +using System.Diagnostics; +using Redmine.Net.Api.Net.Internal; + +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; + +public sealed class RedmineApiUrlsFixture +{ + internal string Format { get; private set; } + + public RedmineApiUrlsFixture() + { + SetMimeTypeJson(); + SetMimeTypeXml(); + + Sut = new RedmineApiUrls(Format); + } + + internal RedmineApiUrls Sut { get; } + + [Conditional("DEBUG_JSON")] + private void SetMimeTypeJson() + { + Format = "json"; + } + + [Conditional("DEBUG_XML")] + private void SetMimeTypeXml() + { + Format = "xml"; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Infrastructure/Fixtures/XmlSerializerFixture.cs b/tests/redmine-net-api.Tests/Infrastructure/Fixtures/XmlSerializerFixture.cs new file mode 100644 index 00000000..55f209ec --- /dev/null +++ b/tests/redmine-net-api.Tests/Infrastructure/Fixtures/XmlSerializerFixture.cs @@ -0,0 +1,9 @@ +using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Xml; + +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; + +public sealed class XmlSerializerFixture +{ + internal IRedmineSerializer Serializer { get; private set; } = new XmlRedmineSerializer(); +} \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Infrastructure/CaseOrder.cs b/tests/redmine-net-api.Tests/Infrastructure/Order/CaseOrder.cs old mode 100755 new mode 100644 similarity index 65% rename from xUnitTest-redmine-net45-api/Infrastructure/CaseOrder.cs rename to tests/redmine-net-api.Tests/Infrastructure/Order/CaseOrder.cs index e267eb1e..b0f8f375 --- a/xUnitTest-redmine-net45-api/Infrastructure/CaseOrder.cs +++ b/tests/redmine-net-api.Tests/Infrastructure/Order/CaseOrder.cs @@ -1,21 +1,20 @@ +#if !(NET20 || NET40) using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using Xunit.Abstractions; using Xunit.Sdk; -namespace xUnitTestredminenet45api +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure.Order { /// /// Custom xUnit test case orderer that uses the OrderAttribute /// - public class CaseOrderer : ITestCaseOrderer + public sealed class CaseOrderer : ITestCaseOrderer { - public const string TYPE_NAME = "xUnitTestredminenet45api.CaseOrderer"; - public const string ASSEMBY_NAME = "xUnitTest-redmine-net45-api"; + // public const string TYPE_NAME = "redmine.net.api.Tests.Infrastructure.CaseOrderer"; + // public const string ASSEMBLY_NAME = "redmine-net-api.Tests"; - public static readonly ConcurrentDictionary> QueuedTests = new ConcurrentDictionary>(); + private static readonly ConcurrentDictionary> QueuedTests = new ConcurrentDictionary>(); public IEnumerable OrderTestCases(IEnumerable testCases) where TTestCase : ITestCase @@ -35,7 +34,9 @@ private static int GetOrder(TTestCase testCase) var attr = testCase.TestMethod.Method .ToRuntimeMethod() .GetCustomAttribute(); - return attr != null ? attr.Index : 0; + + return attr?.Index ?? 0; } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Infrastructure/CollectionOrderer.cs b/tests/redmine-net-api.Tests/Infrastructure/Order/CollectionOrderer.cs old mode 100755 new mode 100644 similarity index 61% rename from xUnitTest-redmine-net45-api/Infrastructure/CollectionOrderer.cs rename to tests/redmine-net-api.Tests/Infrastructure/Order/CollectionOrderer.cs index 3136c1e9..b1ad5a08 --- a/xUnitTest-redmine-net45-api/Infrastructure/CollectionOrderer.cs +++ b/tests/redmine-net-api.Tests/Infrastructure/Order/CollectionOrderer.cs @@ -1,19 +1,18 @@ -using System; -using System.Collections.Generic; -using System.Linq; +#if !(NET20 || NET40) + using System.Reflection; using Xunit; using Xunit.Abstractions; -namespace xUnitTestredminenet45api +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure.Order { /// /// Custom xUnit test collection orderer that uses the OrderAttribute /// - public class CollectionOrderer : ITestCollectionOrderer + public sealed class CollectionOrderer : ITestCollectionOrderer { - public const string TYPE_NAME = "xUnitTestredminenet45api.CollectionOrderer"; - public const string ASSEMBY_NAME = "xUnitTest-redmine-net45-api"; + // public const string TYPE_NAME = "redmine.net.api.Tests.Infrastructure.CollectionOrderer"; + // public const string ASSEMBLY_NAME = "redmine-net-api.Tests"; public IEnumerable OrderTestCollections(IEnumerable testCollections) { @@ -28,15 +27,22 @@ public IEnumerable OrderTestCollections(IEnumerable private static int GetOrder(ITestCollection testCollection) { - var i = testCollection.DisplayName.LastIndexOf(' '); - if (i <= -1) return 0; + var index = testCollection.DisplayName.LastIndexOf(' '); + if (index <= -1) + { + return 0; + } - var className = testCollection.DisplayName.Substring(i + 1); + var className = testCollection.DisplayName.Substring(index + 1); var type = Type.GetType(className); - if (type == null) return 0; + if (type == null) + { + return 0; + } var attr = type.GetCustomAttribute(); - return attr != null ? attr.Index : 0; + return attr?.Index ?? 0; } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Infrastructure/OrderAttribute.cs b/tests/redmine-net-api.Tests/Infrastructure/Order/OrderAttribute.cs old mode 100755 new mode 100644 similarity index 57% rename from xUnitTest-redmine-net45-api/Infrastructure/OrderAttribute.cs rename to tests/redmine-net-api.Tests/Infrastructure/Order/OrderAttribute.cs index 698e785f..1c7e08e7 --- a/xUnitTest-redmine-net45-api/Infrastructure/OrderAttribute.cs +++ b/tests/redmine-net-api.Tests/Infrastructure/Order/OrderAttribute.cs @@ -1,8 +1,6 @@ -using System; - -namespace xUnitTestredminenet45api +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure.Order { - public class OrderAttribute : Attribute + public sealed class OrderAttribute : Attribute { public OrderAttribute(int index) { diff --git a/tests/redmine-net-api.Tests/Properties/launchSettings.json b/tests/redmine-net-api.Tests/Properties/launchSettings.json new file mode 100644 index 00000000..18db8cac --- /dev/null +++ b/tests/redmine-net-api.Tests/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "redmine-net-api.Tests": { + "commandName": "Project", + "environmentVariables": { + "BitVault410": "bitVault410", + "Local410": "local410" + } + } + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Json/AttachmentTests.cs b/tests/redmine-net-api.Tests/Serialization/Json/AttachmentTests.cs new file mode 100644 index 00000000..bd34ff91 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Json/AttachmentTests.cs @@ -0,0 +1,41 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Json; + +[Collection(Constants.JsonRedmineSerializerCollection)] +public class AttachmentTests(JsonSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Attachment() + { + const string input = """ + { + "attachment": { + "id": 6243, + "filename": "test.txt", + "filesize": 124, + "content_type": "text/plain", + "description": "This is an attachment", + "content_url": "http://localhost:3000/attachments/download/6243/test.txt", + "author": {"name": "Jean-Philippe Lang", "id": 1}, + "created_on": "2011-07-18T22:58:40+02:00" + } + } + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal(6243, output.Id); + Assert.Equal("test.txt", output.FileName); + Assert.Equal(124, output.FileSize); + Assert.Equal("text/plain", output.ContentType); + Assert.Equal("This is an attachment", output.Description); + Assert.Equal("http://localhost:3000/attachments/download/6243/test.txt", output.ContentUrl); + Assert.Equal("Jean-Philippe Lang", output.Author.Name); + Assert.Equal(1, output.Author.Id); + Assert.Equal(new DateTime(2011, 7, 18, 20, 58, 40, DateTimeKind.Utc).ToLocalTime(), output.CreatedOn); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Json/CustomFieldTests.cs b/tests/redmine-net-api.Tests/Serialization/Json/CustomFieldTests.cs new file mode 100644 index 00000000..18366876 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Json/CustomFieldTests.cs @@ -0,0 +1,66 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Json; + +[Collection(Constants.JsonRedmineSerializerCollection)] +public sealed class CustomFieldTests(JsonSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_CustomFields() + { + const string input = """ + { + "custom_fields": [ + { + "id": 1, + "name": "Affected version", + "customized_type": "issue", + "field_format": "list", + "regexp": null, + "min_length": null, + "max_length": null, + "is_required": true, + "is_filter": true, + "searchable": true, + "multiple": true, + "default_value": null, + "visible": false, + "possible_values": [ + { + "value": "0.5.x" + }, + { + "value": "0.6.x" + } + ] + } + ], + "total_count": 1 + } + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(1, output.TotalItems); + + var customFields = output.Items.ToList(); + Assert.Equal(1, customFields[0].Id); + Assert.Equal("Affected version", customFields[0].Name); + Assert.Equal("issue", customFields[0].CustomizedType); + Assert.Equal("list", customFields[0].FieldFormat); + Assert.True(customFields[0].IsRequired); + Assert.True(customFields[0].IsFilter); + Assert.True(customFields[0].Searchable); + Assert.True(customFields[0].Multiple); + Assert.False(customFields[0].Visible); + + var possibleValues = customFields[0].PossibleValues.ToList(); + Assert.Equal(2, possibleValues.Count); + Assert.Equal("0.5.x", possibleValues[0].Value); + Assert.Equal("0.6.x", possibleValues[1].Value); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Json/ErrorTests.cs b/tests/redmine-net-api.Tests/Serialization/Json/ErrorTests.cs new file mode 100644 index 00000000..889ac9dc --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Json/ErrorTests.cs @@ -0,0 +1,33 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Json; + +[Collection(Constants.JsonRedmineSerializerCollection)] +public class ErrorTests(JsonSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Errors() + { + const string input = """ + { + "errors":[ + "First name can't be blank", + "Email is invalid" + ], + "total_count":2 + } + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var errors = output.Items.ToList(); + Assert.Equal("First name can't be blank", errors[0].Info); + Assert.Equal("Email is invalid", errors[1].Info); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Json/IssueCustomFieldsTests.cs b/tests/redmine-net-api.Tests/Serialization/Json/IssueCustomFieldsTests.cs new file mode 100644 index 00000000..ee570b6d --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Json/IssueCustomFieldsTests.cs @@ -0,0 +1,43 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Json; + +[Collection(Constants.JsonRedmineSerializerCollection)] +public class IssueCustomFieldsTests(JsonSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Issue_With_CustomFields_With_Multiple_Values() + { + const string input = """ + { + "custom_fields":[ + {"value":["1.0.1","1.0.2"],"multiple":true,"name":"Affected version","id":1}, + {"value":"Fixed","name":"Resolution","id":2} + ], + "total_count":2 + } + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var customFields = output.Items.ToList(); + + Assert.Equal(1, customFields[0].Id); + Assert.Equal("Affected version", customFields[0].Name); + Assert.True(customFields[0].Multiple); + Assert.Equal(2, customFields[0].Values.Count); + Assert.Equal("1.0.1", customFields[0].Values[0].Info); + Assert.Equal("1.0.2", customFields[0].Values[1].Info); + + Assert.Equal(2, customFields[1].Id); + Assert.Equal("Resolution", customFields[1].Name); + Assert.False(customFields[1].Multiple); + Assert.Equal("Fixed", customFields[1].Values[0].Info); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Json/IssuesTests.cs b/tests/redmine-net-api.Tests/Serialization/Json/IssuesTests.cs new file mode 100644 index 00000000..adb73da7 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Json/IssuesTests.cs @@ -0,0 +1,97 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Json; + +[Collection(Constants.JsonRedmineSerializerCollection)] +public class IssuesTests(JsonSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Issue_With_Watchers() + { + const string input = """ + { + "issue": { + "id": 5, + "project": { + "id": 1, + "name": "Project-Test" + }, + "tracker": { + "id": 1, + "name": "Bug" + }, + "status": { + "id": 1, + "name": "New", + "is_closed": true + }, + "priority": { + "id": 2, + "name": "Normal" + }, + "author": { + "id": 90, + "name": "Admin User" + }, + "fixed_version": { + "id": 2, + "name": "version2" + }, + "subject": "#380", + "description": "", + "start_date": "2025-04-28", + "due_date": null, + "done_ratio": 0, + "is_private": false, + "estimated_hours": null, + "total_estimated_hours": null, + "spent_hours": 0.0, + "total_spent_hours": 0.0, + "created_on": "2025-04-28T17:58:42Z", + "updated_on": "2025-04-28T17:58:42Z", + "closed_on": null, + "watchers": [ + { + "id": 91, + "name": "Normal User" + }, + { + "id": 90, + "name": "Admin User" + } + ] + } + } + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal(5, output.Id); + Assert.Equal("Project-Test", output.Project.Name); + Assert.Equal(1, output.Project.Id); + Assert.Equal("Bug", output.Tracker.Name); + Assert.Equal(1, output.Tracker.Id); + Assert.Equal("New", output.Status.Name); + Assert.Equal(1, output.Status.Id); + Assert.True(output.Status.IsClosed); + Assert.False(output.Status.IsDefault); + Assert.Equal(2, output.FixedVersion.Id); + Assert.Equal("version2", output.FixedVersion.Name); + Assert.Equal(new DateTime(2025, 4, 28), output.StartDate); + Assert.Null(output.DueDate); + Assert.Equal(0, output.DoneRatio); + Assert.Null(output.EstimatedHours); + Assert.Null(output.TotalEstimatedHours); + + var watchers = output.Watchers.ToList(); + Assert.Equal(2, watchers.Count); + + Assert.Equal(91, watchers[0].Id); + Assert.Equal("Normal User", watchers[0].Name); + Assert.Equal(90, watchers[1].Id); + Assert.Equal("Admin User", watchers[1].Name); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Json/MyAccount.cs b/tests/redmine-net-api.Tests/Serialization/Json/MyAccount.cs new file mode 100644 index 00000000..9aa09515 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Json/MyAccount.cs @@ -0,0 +1,56 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Json; + +[Collection(Constants.JsonRedmineSerializerCollection)] +public class MyAccount(JsonSerializerFixture fixture) +{ + [Fact] + public void Test_Xml_Serialization() + { + const string input = """ + { + "user": { + "id": 3, + "login": "dlopper", + "admin": false, + "firstname": "Dave", + "lastname": "Lopper", + "mail": "dlopper@somenet.foo", + "created_on": "2006-07-19T17:33:19Z", + "last_login_on": "2020-06-14T13:03:34Z", + "api_key": "c308a59c9dea95920b13522fb3e0fb7fae4f292d", + "custom_fields": [ + { + "id": 4, + "name": "Phone number", + "value": null + }, + { + "id": 5, + "name": "Money", + "value": null + } + ] + } + } + """; + + var output = fixture.Serializer.Deserialize(input); + Assert.Equal(3, output.Id); + Assert.Equal("dlopper", output.Login); + Assert.False(output.IsAdmin); + Assert.Equal("Dave", output.FirstName); + Assert.Equal("Lopper", output.LastName); + Assert.Equal("dlopper@somenet.foo", output.Email); + Assert.Equal("c308a59c9dea95920b13522fb3e0fb7fae4f292d", output.ApiKey); + Assert.NotNull(output.CustomFields); + Assert.Equal(2, output.CustomFields.Count); + Assert.Equal("Phone number", output.CustomFields[0].Name); + Assert.Equal(4, output.CustomFields[0].Id); + Assert.Equal("Money", output.CustomFields[1].Name); + Assert.Equal(5, output.CustomFields[1].Id); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Json/RoleTests.cs b/tests/redmine-net-api.Tests/Serialization/Json/RoleTests.cs new file mode 100644 index 00000000..2434ea18 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Json/RoleTests.cs @@ -0,0 +1,45 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Json; + +[Collection(Constants.JsonRedmineSerializerCollection)] +public sealed class RoleTests(JsonSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Role_And_Permissions() + { + const string input = """ + { + "role": { + "id": 5, + "name": "Reporter", + "assignable": true, + "issues_visibility": "default", + "time_entries_visibility": "all", + "users_visibility": "all", + "permissions": [ + "view_issues", + "add_issues", + "add_issue_notes", + ] + } + } + """; + + var role = fixture.Serializer.Deserialize(input); + + Assert.Equal(5, role.Id); + Assert.Equal("Reporter", role.Name); + Assert.True(role.IsAssignable); + Assert.Equal("default", role.IssuesVisibility); + Assert.Equal("all", role.TimeEntriesVisibility); + Assert.Equal("all", role.UsersVisibility); + Assert.Equal(3, role.Permissions.Count); + Assert.Equal("view_issues", role.Permissions[0].Info); + Assert.Equal("add_issues", role.Permissions[1].Info); + Assert.Equal("add_issue_notes", role.Permissions[2].Info); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Json/UserTests.cs b/tests/redmine-net-api.Tests/Serialization/Json/UserTests.cs new file mode 100644 index 00000000..d7c13243 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Json/UserTests.cs @@ -0,0 +1,50 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Json; + +[Collection(Constants.JsonRedmineSerializerCollection)] +public class UserTests(JsonSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_User() + { + const string input = """ + { + "user":{ + "id": 3, + "login":"jplang", + "firstname": "Jean-Philippe", + "lastname":"Lang", + "mail":"jp_lang@yahoo.fr", + "created_on": "2007-09-28T00:16:04+02:00", + "updated_on":"2010-08-01T18:05:45+02:00", + "last_login_on":"2011-08-01T18:05:45+02:00", + "passwd_changed_on": "2011-08-01T18:05:45+02:00", + "api_key": "ebc3f6b781a6fb3f2b0a83ce0ebb80e0d585189d", + "avatar_url": "", + "status": 1 + } + } + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal(3, output.Id); + Assert.Equal("jplang", output.Login); + Assert.Equal("Jean-Philippe", output.FirstName); + Assert.Equal("Lang", output.LastName); + Assert.Equal("jp_lang@yahoo.fr", output.Email); + Assert.Equal(new DateTime(2007, 9, 28, 0, 16, 4, DateTimeKind.Local).AddHours(1), output.CreatedOn); + Assert.Equal(new DateTime(2010, 8, 1, 18, 5, 45, DateTimeKind.Local).AddHours(1), output.UpdatedOn); + Assert.Equal(new DateTime(2011, 8, 1, 18, 5, 45, DateTimeKind.Local).AddHours(1), output.LastLoginOn); + Assert.Equal(new DateTime(2011, 8, 1, 18, 5, 45, DateTimeKind.Local).AddHours(1), output.PasswordChangedOn); + Assert.Equal("ebc3f6b781a6fb3f2b0a83ce0ebb80e0d585189d", output.ApiKey); + Assert.Empty(output.AvatarUrl); + Assert.Equal(UserStatus.StatusActive, output.Status); + } + +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/AttachmentTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/AttachmentTests.cs new file mode 100644 index 00000000..9930b97d --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/AttachmentTests.cs @@ -0,0 +1,42 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class AttachmentTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Attachment() + { + const string input = """ + + + 6243 + test.txt + 124 + text/plain + This is an attachment + http://localhost:3000/attachments/download/6243/test.txt + + 2011-07-18T22:58:40+02:00 + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal(6243, output.Id); + Assert.Equal("test.txt", output.FileName); + Assert.Equal(124, output.FileSize); + Assert.Equal("text/plain", output.ContentType); + Assert.Equal("This is an attachment", output.Description); + Assert.Equal("http://localhost:3000/attachments/download/6243/test.txt", output.ContentUrl); + Assert.Equal("Jean-Philippe Lang", output.Author.Name); + Assert.Equal(1, output.Author.Id); + Assert.Equal(new DateTime(2011, 7, 18, 20, 58, 40, DateTimeKind.Utc).ToLocalTime(), output.CreatedOn); + + } +} + diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/CustomFieldTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/CustomFieldTests.cs new file mode 100644 index 00000000..daa7a02d --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/CustomFieldTests.cs @@ -0,0 +1,64 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public sealed class CustomFieldTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_CustomFields() + { + const string input = """ + + + + 1 + Affected version + issue + list + + + + true + true + true + true + + false + + + 0.5.x + + + 0.6.x + + + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(1, output.TotalItems); + + var customFields = output.Items.ToList(); + Assert.Equal(1, customFields[0].Id); + Assert.Equal("Affected version", customFields[0].Name); + Assert.Equal("issue", customFields[0].CustomizedType); + Assert.Equal("list", customFields[0].FieldFormat); + Assert.True(customFields[0].IsRequired); + Assert.True(customFields[0].IsFilter); + Assert.True(customFields[0].Searchable); + Assert.True(customFields[0].Multiple); + Assert.False(customFields[0].Visible); + + var possibleValues = customFields[0].PossibleValues.ToList(); + Assert.Equal(2, possibleValues.Count); + Assert.Equal("0.5.x", possibleValues[0].Value); + Assert.Equal("0.6.x", possibleValues[1].Value); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/EnumerationTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/EnumerationTests.cs new file mode 100644 index 00000000..3c0a6740 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/EnumerationTests.cs @@ -0,0 +1,116 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class EnumerationTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Issue_Priorities() + { + const string input = """ + + + + 3 + Low + false + + + 4 + Normal + true + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var issuePriorities = output.Items.ToList(); + Assert.Equal(2, issuePriorities.Count); + + Assert.Equal(3, issuePriorities[0].Id); + Assert.Equal("Low", issuePriorities[0].Name); + Assert.False(issuePriorities[0].IsDefault); + + Assert.Equal(4, issuePriorities[1].Id); + Assert.Equal("Normal", issuePriorities[1].Name); + Assert.True(issuePriorities[1].IsDefault); + } + + [Fact] + public void Should_Deserialize_TimeEntry_Activities() + { + const string input = """ + + + + 8 + Design + false + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Single(output.Items); + + var timeEntryActivities = output.Items.ToList(); + Assert.Equal(8, timeEntryActivities[0].Id); + Assert.Equal("Design", timeEntryActivities[0].Name); + Assert.False(timeEntryActivities[0].IsDefault); + } + + [Fact] + public void Should_Deserialize_Document_Categories() + { + const string input = """ + + + + 1 + Uncategorized + false + + + 2 + User documentation + false + + + 3 + Technical documentation + false + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(3, output.TotalItems); + + var documentCategories = output.Items.ToList(); + Assert.Equal(3, documentCategories.Count); + + Assert.Equal(1, documentCategories[0].Id); + Assert.Equal("Uncategorized", documentCategories[0].Name); + Assert.False(documentCategories[0].IsDefault); + + Assert.Equal(2, documentCategories[1].Id); + Assert.Equal("User documentation", documentCategories[1].Name); + Assert.False(documentCategories[1].IsDefault); + + Assert.Equal(3, documentCategories[2].Id); + Assert.Equal("Technical documentation", documentCategories[2].Name); + Assert.False(documentCategories[2].IsDefault); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/ErrorTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/ErrorTests.cs new file mode 100644 index 00000000..a40bd64a --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/ErrorTests.cs @@ -0,0 +1,31 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public sealed class ErrorTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Errors() + { + const string input = """ + + First name can't be blank + Email is invalid + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var errors = output.Items.ToList(); + Assert.Equal("First name can't be blank", errors[0].Info); + Assert.Equal("Email is invalid", errors[1].Info); + + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/FileTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/FileTests.cs new file mode 100644 index 00000000..1adc3c59 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/FileTests.cs @@ -0,0 +1,85 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; +using File = Redmine.Net.Api.Types.File; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class FileTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_File() + { + const string input = """ + + + 12 + foo-1.0-setup.exe + 74753799 + application/octet-stream + Foo App for Windows + http://localhost:3000/attachments/download/12/foo-1.0-setup.exe + + 2017-01-04T09:12:32Z + + 1276481102f218c981e0324180bafd9f + 12 + + """; + + var output = fixture.Serializer.Deserialize(input); + Assert.NotNull(output); + Assert.Equal(12, output.Id); + Assert.Equal("foo-1.0-setup.exe", output.Filename); + Assert.Equal("application/octet-stream", output.ContentType); + Assert.Equal("Foo App for Windows", output.Description); + Assert.Equal("http://localhost:3000/attachments/download/12/foo-1.0-setup.exe", output.ContentUrl); + Assert.Equal(1, output.Author.Id); + Assert.Equal("Redmine Admin", output.Author.Name); + Assert.Equal(new DateTimeOffset(new DateTime(2017,01,04,09,12,32, DateTimeKind.Utc)), new DateTimeOffset(output.CreatedOn!.Value)); + Assert.Equal(2, output.Version.Id); + Assert.Equal("1.0", output.Version.Name); + Assert.Equal("1276481102f218c981e0324180bafd9f", output.Digest); + Assert.Equal(12, output.Downloads); + } + + [Fact] + public void Should_Deserialize_Files() + { + const string input = """ + + + + 12 + foo-1.0-setup.exe + 74753799 + application/octet-stream + Foo App for Windows + http://localhost:3000/attachments/download/12/foo-1.0-setup.exe + + 2017-01-04T09:12:32Z + + 1276481102f218c981e0324180bafd9f + 12 + + + 11 + foo-1.0.dmg + 6886287 + application/x-octet-stream + Foo App for macOS + http://localhost:3000/attachments/download/11/foo-1.0.dmg + + 2017-01-04T09:12:07Z + + 14758f1afd44c09b7992073ccf00b43d + 5 + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + Assert.NotNull(output); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/GroupTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/GroupTests.cs new file mode 100644 index 00000000..d563fb41 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/GroupTests.cs @@ -0,0 +1,65 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class GroupTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Group() + { + const string input = """ + + 20 + Developers + + + + + + """; + + var output = fixture.Serializer.Deserialize(input); + Assert.NotNull(output); + Assert.Equal(20, output.Id); + Assert.Equal("Developers", output.Name); + Assert.NotNull(output.Users); + Assert.Equal(2, output.Users.Count); + Assert.Equal("John Smith", output.Users[0].Name); + Assert.Equal("Dave Loper", output.Users[1].Name); + Assert.Equal(5, output.Users[0].Id); + Assert.Equal(8, output.Users[1].Id); + } + + [Fact] + public void Should_Deserialize_Groups() + { + const string input = """ + + + + 53 + Managers + + + 55 + Developers + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var groups = output.Items.ToList(); + Assert.Equal(53, groups[0].Id); + Assert.Equal("Managers", groups[0].Name); + + Assert.Equal(55, groups[1].Id); + Assert.Equal("Developers", groups[1].Name); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/IssueCategoryTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/IssueCategoryTests.cs new file mode 100644 index 00000000..3b658f55 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/IssueCategoryTests.cs @@ -0,0 +1,71 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class IssueCategoryTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Issue_Category() + { + const string input = """ + + + 2 + + UI + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal(2, output.Id); + Assert.Equal("Redmine", output.Project.Name); + Assert.Equal(1, output.Project.Id); + Assert.Equal("UI", output.Name); + } + + [Fact] + public void Should_Deserialize_Issue_Categories() + { + const string input = """ + + + + 57 + + UI + + + + 58 + + Test + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var issueCategories = output.Items.ToList(); + Assert.Equal(2, issueCategories.Count); + + Assert.Equal(57, issueCategories[0].Id); + Assert.Equal("Foo", issueCategories[0].Project.Name); + Assert.Equal(17, issueCategories[0].Project.Id); + Assert.Equal("UI", issueCategories[0].Name); + Assert.Equal("John Smith", issueCategories[0].AssignTo.Name); + Assert.Equal(22, issueCategories[0].AssignTo.Id); + + Assert.Equal(58, issueCategories[1].Id); + Assert.Equal("Foo", issueCategories[1].Project.Name); + Assert.Equal(17, issueCategories[1].Project.Id); + Assert.Equal("Test", issueCategories[1].Name); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/IssueStatusTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/IssueStatusTests.cs new file mode 100644 index 00000000..58daceca --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/IssueStatusTests.cs @@ -0,0 +1,46 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class IssueStatusTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Issue_Statuses() + { + const string input = """ + + + + 1 + New + false + + + 2 + Closed + true + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var issueStatuses = output.Items.ToList(); + Assert.Equal(2, issueStatuses.Count); + + Assert.Equal(1, issueStatuses[0].Id); + Assert.Equal("New", issueStatuses[0].Name); + Assert.False(issueStatuses[0].IsClosed); + + Assert.Equal(2, issueStatuses[1].Id); + Assert.Equal("Closed", issueStatuses[1].Name); + Assert.True(issueStatuses[1].IsClosed); + } +} + diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/IssueTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/IssueTests.cs new file mode 100644 index 00000000..a423b713 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/IssueTests.cs @@ -0,0 +1,304 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class IssueTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Issues() + { + const string input = """ + + + + 4326 + + + + + + + Aggregate Multiple Issue Changes for Email Notifications + + This is not to be confused with another useful proposed feature that + would do digest emails for notifications. + + 2009-12-03 + + 0 + + Thu Dec 03 15:02:12 +0100 2009 + Sun Jan 03 12:08:41 +0100 2010 + + + 4325 + + + + + + + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(1640, output.TotalItems); + + var issues = output.Items.ToList(); + Assert.Equal(4326, issues[0].Id); + Assert.Equal("Redmine", issues[0].Project.Name); + Assert.Equal(1, issues[0].Project.Id); + Assert.Equal("Feature", issues[0].Tracker.Name); + Assert.Equal(2, issues[0].Tracker.Id); + Assert.Equal("New", issues[0].Status.Name); + Assert.Equal(1, issues[0].Status.Id); + Assert.Equal("Normal", issues[0].Priority.Name); + Assert.Equal(4, issues[0].Priority.Id); + Assert.Equal("John Smith", issues[0].Author.Name); + Assert.Equal(10106, issues[0].Author.Id); + Assert.Equal("Email notifications", issues[0].Category.Name); + Assert.Equal(9, issues[0].Category.Id); + Assert.Equal("Aggregate Multiple Issue Changes for Email Notifications", issues[0].Subject); + Assert.Contains("This is not to be confused with another useful proposed feature", issues[0].Description); + Assert.Equal(new DateTime(2009, 12, 3), issues[0].StartDate); + Assert.Null(issues[0].DueDate); + Assert.Equal(0, issues[0].DoneRatio); + Assert.Null(issues[0].EstimatedHours); + // Assert.Equal(new DateTime(2009, 12, 3, 14, 2, 12, DateTimeKind.Utc).ToLocalTime(), issues[0].CreatedOn); + // Assert.Equal(new DateTime(2010, 1, 3, 11, 8, 41, DateTimeKind.Utc).ToLocalTime(), issues[0].UpdatedOn); + + Assert.Equal(4325, issues[1].Id); + Assert.Null(issues[1].Journals); + Assert.Null(issues[1].ChangeSets); + Assert.Null(issues[1].CustomFields); + } + + [Fact] + public void Should_Deserialize_Issues_With_CustomFields() + { + const string input = """ + + + + 4326 + + + + + + + + Aggregate Multiple Issue Changes for Email Notifications + + + This is not to be confused with another useful proposed feature that + would do digest emails for notifications. + + 2009-12-03 + + 0 + + + Duplicate + Test + 1 + 2010-01-12 + + Thu Dec 03 15:02:12 +0100 2009 + Sun Jan 03 12:08:41 +0100 2010 + + + 4325 + + + + + 1.0.1 + + + Fixed + + + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var issues = output.Items.ToList(); + Assert.Equal(4326, issues[0].Id); + Assert.Equal("Redmine", issues[0].Project.Name); + Assert.Equal(1, issues[0].Project.Id); + Assert.Equal("Feature", issues[0].Tracker.Name); + Assert.Equal(2, issues[0].Tracker.Id); + Assert.Equal("New", issues[0].Status.Name); + Assert.Equal(1, issues[0].Status.Id); + Assert.Equal("Normal", issues[0].Priority.Name); + Assert.Equal(4, issues[0].Priority.Id); + Assert.Equal("John Smith", issues[0].Author.Name); + Assert.Equal(10106, issues[0].Author.Id); + Assert.Equal("Email notifications", issues[0].Category.Name); + Assert.Equal(9, issues[0].Category.Id); + Assert.Contains("Aggregate Multiple Issue Changes for Email Notifications", issues[0].Subject); + Assert.Contains("This is not to be confused with another useful proposed feature", issues[0].Description); + Assert.Equal(new DateTime(2009, 12, 3), issues[0].StartDate); + Assert.Null(issues[0].DueDate); + Assert.Equal(0, issues[0].DoneRatio); + Assert.Null(issues[0].EstimatedHours); + + Assert.NotNull(issues[0].CustomFields); + var issueCustomFields = issues[0].CustomFields.ToList(); + Assert.Equal(4, issueCustomFields.Count); + + Assert.Equal(2,issueCustomFields[0].Id); + Assert.Equal("Duplicate",issueCustomFields[0].Values[0].Info); + Assert.False(issueCustomFields[0].Multiple); + Assert.Equal("Resolution",issueCustomFields[0].Name); + + Assert.NotNull(issues[1].CustomFields); + issueCustomFields = issues[1].CustomFields.ToList(); + Assert.Equal(2, issueCustomFields.Count); + + Assert.Equal(1,issueCustomFields[0].Id); + Assert.Equal("1.0.1",issueCustomFields[0].Values[0].Info); + Assert.False(issueCustomFields[0].Multiple); + Assert.Equal("Affected version",issueCustomFields[0].Name); + } + + [Fact] + public void Should_Deserialize_Issue_With_Journals() + { + const string input = """ + + 1 + + + + + + Fixed in Revision 128 + 2007-01-01T05:21:00+01:00 +
+ + + + + 2009-08-13T11:33:17+02:00 +
+ + 5 + 8 + +
+
+ + + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal(1, output.Id); + Assert.Equal("Redmine", output.Project.Name); + Assert.Equal(1, output.Project.Id); + Assert.Equal("Defect", output.Tracker.Name); + Assert.Equal(1, output.Tracker.Id); + + var journals = output.Journals.ToList(); + Assert.Equal(2, journals.Count); + + Assert.Equal(1, journals[0].Id); + Assert.Equal("Jean-Philippe Lang", journals[0].User.Name); + Assert.Equal(1, journals[0].User.Id); + Assert.Equal("Fixed in Revision 128", journals[0].Notes); + Assert.Equal(new DateTime(2007, 1, 1, 4, 21, 0, DateTimeKind.Utc).ToLocalTime(), journals[0].CreatedOn); + Assert.Null(journals[0].Details); + + Assert.Equal(10531, journals[1].Id); + Assert.Equal("efgh efgh", journals[1].User.Name); + Assert.Equal(7384, journals[1].User.Id); + Assert.Null(journals[1].Notes); + Assert.Equal(new DateTime(2009, 8, 13, 9, 33, 17, DateTimeKind.Utc).ToLocalTime(), journals[1].CreatedOn); + + var details = journals[1].Details.ToList(); + Assert.Single(details); + Assert.Equal("attr", details[0].Property); + Assert.Equal("status_id", details[0].Name); + Assert.Equal("5", details[0].OldValue); + Assert.Equal("8", details[0].NewValue); + + } + + [Fact] + public void Should_Deserialize_Issue_With_Watchers() + { + const string input = """ + + + 5 + + + + + + + #380 + + 2025-04-28 + + 0 + false + + + 0.0 + 0.0 + 2025-04-28T17:58:42Z + 2025-04-28T17:58:42Z + + + + + + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal(5, output.Id); + Assert.Equal("Project-Test", output.Project.Name); + Assert.Equal(1, output.Project.Id); + Assert.Equal("Bug", output.Tracker.Name); + Assert.Equal(1, output.Tracker.Id); + Assert.Equal("New", output.Status.Name); + Assert.Equal(1, output.Status.Id); + Assert.True(output.Status.IsClosed); + Assert.False(output.Status.IsDefault); + Assert.Equal(2, output.FixedVersion.Id); + Assert.Equal("version2", output.FixedVersion.Name); + Assert.Equal(new DateTime(2025, 4, 28), output.StartDate); + Assert.Null(output.DueDate); + Assert.Equal(0, output.DoneRatio); + Assert.Null(output.EstimatedHours); + Assert.Null(output.TotalEstimatedHours); + + var watchers = output.Watchers.ToList(); + Assert.Equal(2, watchers.Count); + + Assert.Equal(91, watchers[0].Id); + Assert.Equal("Normal User", watchers[0].Name); + Assert.Equal(90, watchers[1].Id); + Assert.Equal("Admin User", watchers[1].Name); + } +} + diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/MembershipTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/MembershipTests.cs new file mode 100644 index 00000000..59aab9b9 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/MembershipTests.cs @@ -0,0 +1,94 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public sealed class MembershipTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Memberships() + { + const string input = """ + + + + 1 + + + + + + + + 3 + + + + + + + + 4 + + + + + + + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + } + + [Fact] + public void Should_Deserialize_Membership() + { + const string input = """ + + + 1 + + + + """; + + var output = fixture.Serializer.Deserialize(input); + Assert.Equal(1, output.Id); + Assert.Equal("Redmine", output.Project.Name); + Assert.Equal(1, output.Project.Id); + Assert.Equal("David Robert", output.User.Name); + Assert.Equal(17, output.User.Id); + } + + [Fact] + public void Should_Deserialize_Membership_With_Roles() + { + const string input = """ + + + 1 + + + + + + + """; + + var output = fixture.Serializer.Deserialize(input); + Assert.Equal(1, output.Id); + Assert.Equal("Redmine", output.Project.Name); + Assert.Equal(1, output.Project.Id); + Assert.Equal("David Robert", output.User.Name); + Assert.Equal(17, output.User.Id); + Assert.NotNull(output.Roles); + Assert.Single(output.Roles); + Assert.Equal("Manager", output.Roles[0].Name); + Assert.Equal(1, output.Roles[0].Id); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/MyAccountTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/MyAccountTests.cs new file mode 100644 index 00000000..ae1b70e2 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/MyAccountTests.cs @@ -0,0 +1,79 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class MyAccountTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_MyAccount() + { + const string input = """ + + + 3 + dlopper + false + Dave + Lopper + dlopper@somenet.foo + 2006-07-19T17:33:19Z + 2020-06-14T13:03:34Z + c308a59c9dea95920b13522fb3e0fb7fae4f292d + + """; + + var output = fixture.Serializer.Deserialize(input); + Assert.Equal(3, output.Id); + Assert.Equal("dlopper", output.Login); + Assert.False(output.IsAdmin); + Assert.Equal("Dave", output.FirstName); + Assert.Equal("Lopper", output.LastName); + Assert.Equal("dlopper@somenet.foo", output.Email); + Assert.Equal("c308a59c9dea95920b13522fb3e0fb7fae4f292d", output.ApiKey); + } + + [Fact] + public void Should_Deserialize_MyAccount_With_CustomFields() + { + const string input = """ + + + 3 + dlopper + false + Dave + Lopper + dlopper@somenet.foo + 2006-07-19T17:33:19Z + 2020-06-14T13:03:34Z + c308a59c9dea95920b13522fb3e0fb7fae4f292d + + + + + + + + + + """; + + var output = fixture.Serializer.Deserialize(input); + Assert.Equal(3, output.Id); + Assert.Equal("dlopper", output.Login); + Assert.False(output.IsAdmin); + Assert.Equal("Dave", output.FirstName); + Assert.Equal("Lopper", output.LastName); + Assert.Equal("dlopper@somenet.foo", output.Email); + Assert.Equal("c308a59c9dea95920b13522fb3e0fb7fae4f292d", output.ApiKey); + Assert.NotNull(output.CustomFields); + Assert.Equal(2, output.CustomFields.Count); + Assert.Equal("Phone number", output.CustomFields[0].Name); + Assert.Equal(4, output.CustomFields[0].Id); + Assert.Equal("Money", output.CustomFields[1].Name); + Assert.Equal(5, output.CustomFields[1].Id); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/NewsTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/NewsTests.cs new file mode 100644 index 00000000..93ebe8d3 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/NewsTests.cs @@ -0,0 +1,65 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class NewsTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_News() + { + const string input = """ + + + + 54 + + + Redmine 1.1.3 released + + Redmine 1.1.3 has been released + 2011-04-29T14:00:25+02:00 + + + 53 + + + Redmine 1.1.2 bug/security fix released + + Redmine 1.1.2 has been released + 2011-03-07T21:07:03+01:00 + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var newsItems = output.Items.ToList(); + Assert.Equal(2, newsItems.Count); + + Assert.Equal(54, newsItems[0].Id); + Assert.Equal("Redmine", newsItems[0].Project.Name); + Assert.Equal(1, newsItems[0].Project.Id); + Assert.Equal("Jean-Philippe Lang", newsItems[0].Author.Name); + Assert.Equal(1, newsItems[0].Author.Id); + Assert.Equal("Redmine 1.1.3 released", newsItems[0].Title); + Assert.Equal("Redmine 1.1.3 has been released", newsItems[0].Description); + Assert.Equal(new DateTime(2011, 4, 29, 12, 0, 25, DateTimeKind.Utc).ToLocalTime(), newsItems[0].CreatedOn); + + Assert.Equal(53, newsItems[1].Id); + Assert.Equal("Redmine", newsItems[1].Project.Name); + Assert.Equal(1, newsItems[1].Project.Id); + Assert.Equal("Jean-Philippe Lang", newsItems[1].Author.Name); + Assert.Equal(1, newsItems[1].Author.Id); + Assert.Equal("Redmine 1.1.2 bug/security fix released", newsItems[1].Title); + Assert.Equal("Redmine 1.1.2 has been released", newsItems[1].Description); + Assert.Equal(new DateTime(2011, 3, 7, 20, 7, 3, DateTimeKind.Utc).ToLocalTime(), newsItems[1].CreatedOn); + + } +} + diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/ProjectTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/ProjectTests.cs new file mode 100644 index 00000000..fa1a9f44 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/ProjectTests.cs @@ -0,0 +1,85 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class ProjectTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Project() + { + const string input = """ + + + 1 + Redmine + redmine + + Redmine is a flexible project management web application written using Ruby on Rails framework. + + + 1 + + + + 2007-09-29T12:03:04+02:00 + 2009-03-15T12:35:11+01:00 + true + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + + Assert.Equal(1, output.Id); + Assert.Equal("Redmine", output.Name); + Assert.Equal("redmine", output.Identifier); + Assert.Contains("Redmine is a flexible project management web application", output.Description); + Assert.Equal(new DateTime(2007, 9, 29, 10, 3, 4, DateTimeKind.Utc).ToLocalTime(), output.CreatedOn); + Assert.Equal(new DateTime(2009, 3, 15, 11, 35, 11, DateTimeKind.Utc).ToLocalTime(), output.UpdatedOn); + Assert.True(output.IsPublic); + } + + [Fact] + public void Should_Deserialize_Projects() + { + const string input = """ + + + + 1 + Redmine + redmine + + Redmine is a flexible project management web application written using Ruby on Rails framework. + + 2007-09-29T12:03:04+02:00 + 2009-03-15T12:35:11+01:00 + true + + + 2 + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var projects = output.Items.ToList(); + Assert.Equal(1, projects[0].Id); + Assert.Equal("Redmine", projects[0].Name); + Assert.Equal("redmine", projects[0].Identifier); + Assert.Contains("Redmine is a flexible project management web application", projects[0].Description); + Assert.Equal(new DateTime(2007, 9, 29, 10, 3, 4, DateTimeKind.Utc).ToLocalTime(), projects[0].CreatedOn); + Assert.Equal(new DateTime(2009, 3, 15, 11, 35, 11, DateTimeKind.Utc).ToLocalTime(), projects[0].UpdatedOn); + Assert.True(projects[0].IsPublic); + + Assert.Equal(2, projects[1].Id); + } +} diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/QueryTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/QueryTests.cs new file mode 100644 index 00000000..9e830935 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/QueryTests.cs @@ -0,0 +1,50 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class QueryTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Version() + { + const string input = """ + + + + 84 + Documentation issues + true + 1 + + + 1 + Open defects + true + 1 + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(5, output.TotalItems); + + var queries = output.Items.ToList(); + Assert.Equal(2, queries.Count); + + Assert.Equal(84, queries[0].Id); + Assert.Equal("Documentation issues", queries[0].Name); + Assert.True(queries[0].IsPublic); + Assert.Equal(1, queries[0].ProjectId); + + Assert.Equal(1, queries[1].Id); + Assert.Equal("Open defects", queries[1].Name); + Assert.True(queries[1].IsPublic); + Assert.Equal(1, queries[1].ProjectId); + } +} + diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/RelationTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/RelationTests.cs new file mode 100644 index 00000000..cc9ecd5b --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/RelationTests.cs @@ -0,0 +1,80 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class RelationTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Relation() + { + const string input = """ + + + 1819 + 8470 + 8469 + relates + + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal(1819, output.Id); + Assert.Equal(8470, output.IssueId); + Assert.Equal(8469, output.IssueToId); + Assert.Equal(IssueRelationType.Relates, output.Type); + Assert.Null(output.Delay); + } + + [Fact] + public void Should_Deserialize_Relations() + { + const string input = """ + + + + 1819 + 8470 + 8469 + relates + + + + 1820 + 8470 + 8467 + relates + + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var relations = output.Items.ToList(); + Assert.Equal(2, relations.Count); + + Assert.Equal(1819, relations[0].Id); + Assert.Equal(8470, relations[0].IssueId); + Assert.Equal(8469, relations[0].IssueToId); + Assert.Equal(IssueRelationType.Relates, relations[0].Type); + Assert.Null(relations[0].Delay); + + Assert.Equal(1820, relations[1].Id); + Assert.Equal(8470, relations[1].IssueId); + Assert.Equal(8467, relations[1].IssueToId); + Assert.Equal(IssueRelationType.Relates, relations[1].Type); + Assert.Null(relations[1].Delay); + } +} + + diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/RoleTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/RoleTests.cs new file mode 100644 index 00000000..2790bc89 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/RoleTests.cs @@ -0,0 +1,94 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public sealed class RoleTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Role() + { + const string input = """ + + 5 + Reporter + true + default + all + all + + """; + var role = fixture.Serializer.Deserialize(input); + + Assert.Equal(5, role.Id); + Assert.Equal("Reporter", role.Name); + Assert.True(role.IsAssignable); + Assert.Equal("default", role.IssuesVisibility); + Assert.Equal("all", role.TimeEntriesVisibility); + Assert.Equal("all", role.UsersVisibility); + } + + [Fact] + public void Should_Deserialize_Role_And_Permissions() + { + const string input = """ + + 5 + Reporter + true + default + all + all + + view_issues + add_issues + add_issue_notes + + + """; + var role = fixture.Serializer.Deserialize(input); + + Assert.Equal(5, role.Id); + Assert.Equal("Reporter", role.Name); + Assert.True(role.IsAssignable); + Assert.Equal("default", role.IssuesVisibility); + Assert.Equal("all", role.TimeEntriesVisibility); + Assert.Equal("all", role.UsersVisibility); + Assert.Equal(3, role.Permissions.Count); + Assert.Equal("view_issues", role.Permissions[0].Info); + Assert.Equal("add_issues", role.Permissions[1].Info); + Assert.Equal("add_issue_notes", role.Permissions[2].Info); + } + + [Fact] + public void Should_Deserialize_Roles() + { + const string input = """ + + + + 1 + Manager + + + 2 + Developer + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var roles = output.Items.ToList(); + Assert.Equal(1, roles[0].Id); + Assert.Equal("Manager", roles[0].Name); + + Assert.Equal(2, roles[1].Id); + Assert.Equal("Developer", roles[1].Name); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/SearchTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/SearchTests.cs new file mode 100644 index 00000000..928cd089 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/SearchTests.cs @@ -0,0 +1,57 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public sealed class SearchTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Search_Result() + { + const string input = """ + + + 5 + Wiki: Wiki_Page_Name + wiki-page + http://www.redmine.org/projects/new_crm_dev/wiki/Wiki_Page_Name + h1. Wiki Page Name wiki_keyword + 2016-03-25T05:23:35Z + + + 10 + Issue #10 (Closed): Issue_Title + issue closed + http://www.redmin.org/issues/10 + issue_keyword + 2016-03-24T05:18:59Z + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + Assert.Equal(25, output.PageSize); + + var results = output.Items.ToList(); + Assert.Equal(5, results[0].Id); + Assert.Equal("Wiki: Wiki_Page_Name", results[0].Title); + Assert.Equal("wiki-page", results[0].Type); + Assert.Equal("http://www.redmine.org/projects/new_crm_dev/wiki/Wiki_Page_Name", results[0].Url); + Assert.Equal("h1. Wiki Page Name wiki_keyword", results[0].Description); + Assert.Equal(new DateTime(2016, 3, 25, 5, 23, 35, DateTimeKind.Utc).ToLocalTime(), results[0].DateTime); + + Assert.Equal(10, results[1].Id); + Assert.Equal("Issue #10 (Closed): Issue_Title", results[1].Title); + Assert.Equal("issue closed", results[1].Type); + Assert.Equal("http://www.redmin.org/issues/10", results[1].Url); + Assert.Equal("issue_keyword", results[1].Description); + Assert.Equal(new DateTime(2016, 3, 24, 5, 18, 59, DateTimeKind.Utc).ToLocalTime(), results[1].DateTime); + + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/TrackerTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/TrackerTests.cs new file mode 100644 index 00000000..4d39faa8 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/TrackerTests.cs @@ -0,0 +1,133 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class TrackerTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Tracker() + { + const string input = """ + + + 1 + Defect + + Description for Bug tracker + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + + Assert.Equal(1, output.Id); + Assert.Equal("Defect", output.Name); + Assert.Equal("New", output.DefaultStatus.Name); + Assert.Equal("Description for Bug tracker", output.Description); + } + + [Fact] + public void Should_Deserialize_Tracker_With_Enumerations() + { + const string input = """ + + + 1 + Defect + + Description for Bug tracker + + assigned_to_id + category_id + fixed_version_id + parent_issue_id + start_date + due_date + estimated_hours + done_ratio + description + + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + + Assert.Equal(1, output.Id); + Assert.Equal("Defect", output.Name); + Assert.Equal("New", output.DefaultStatus.Name); + Assert.Equal("Description for Bug tracker", output.Description); + Assert.Equal(9, output.EnabledStandardFields.Count); + } + + [Fact] + public void Should_Deserialize_Trackers() + { + const string input = """ + + + + 1 + Defect + + Description for Bug tracker + + assigned_to_id + category_id + fixed_version_id + parent_issue_id + start_date + due_date + estimated_hours + done_ratio + description + + + + 2 + Feature + + Description for Feature request tracker + + assigned_to_id + category_id + fixed_version_id + parent_issue_id + start_date + due_date + estimated_hours + done_ratio + description + + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var trackers = output.Items.ToList(); + Assert.Equal(2, trackers.Count); + + Assert.Equal(1, trackers[0].Id); + Assert.Equal("Defect", trackers[0].Name); + Assert.Equal("New", trackers[0].DefaultStatus.Name); + Assert.Equal("Description for Bug tracker", trackers[0].Description); + Assert.Equal(9, trackers[0].EnabledStandardFields.Count); + + Assert.Equal(2, trackers[1].Id); + Assert.Equal("Feature", trackers[1].Name); + Assert.Equal("New", trackers[1].DefaultStatus.Name); + Assert.Equal("Description for Feature request tracker", trackers[1].Description); + Assert.Equal(9, trackers[1].EnabledStandardFields.Count); + + } +} diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/UploadTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/UploadTests.cs new file mode 100644 index 00000000..b9693b1a --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/UploadTests.cs @@ -0,0 +1,61 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class UploadTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Upload() + { + const string input = """ + + + #{token1} + test1.txt + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal("#{token1}", output.Token); + Assert.Equal("test1.txt", output.FileName); + } + + [Fact] + public void Should_Deserialize_Uploads() + { + const string input = """ + + + + #{token1} + test1.txt + + + #{token2} + test1.txt + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var uploads = output.Items.ToList(); + Assert.Equal(2, uploads.Count); + + Assert.Equal("#{token1}", uploads[0].Token); + Assert.Equal("test1.txt", uploads[0].FileName); + + Assert.Equal("#{token2}", uploads[1].Token); + Assert.Equal("test1.txt", uploads[1].FileName); + + } +} + diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/UserTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/UserTests.cs new file mode 100644 index 00000000..39f31e9a --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/UserTests.cs @@ -0,0 +1,156 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class UserTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_User() + { + const string input = """ + + + 3 + jplang + Jean-Philippe + Lang + jp_lang@yahoo.fr + 2007-09-28T00:16:04+02:00 + 2010-08-01T18:05:45+02:00 + 2011-08-01T18:05:45+02:00 + 2011-08-01T18:05:45+02:00 + ebc3f6b781a6fb3f2b0a83ce0ebb80e0d585189d + + 1 + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal(3, output.Id); + Assert.Equal("jplang", output.Login); + Assert.Equal("Jean-Philippe", output.FirstName); + Assert.Equal("Lang", output.LastName); + Assert.Equal("jp_lang@yahoo.fr", output.Email); + Assert.Equal(new DateTime(2007, 9, 28, 0, 16, 4, DateTimeKind.Local).AddHours(1), output.CreatedOn); + Assert.Equal(new DateTime(2010, 8, 1, 18, 5, 45, DateTimeKind.Local).AddHours(1), output.UpdatedOn); + Assert.Equal(new DateTime(2011, 8, 1, 18, 5, 45, DateTimeKind.Local).AddHours(1), output.LastLoginOn); + Assert.Equal(new DateTime(2011, 8, 1, 18, 5, 45, DateTimeKind.Local).AddHours(1), output.PasswordChangedOn); + Assert.Equal("ebc3f6b781a6fb3f2b0a83ce0ebb80e0d585189d", output.ApiKey); + Assert.Empty(output.AvatarUrl); + Assert.Equal(UserStatus.StatusActive, output.Status); + } + + [Fact] + public void Should_Deserialize_User_With_Memberships() + { + const string input = """ + + + 3 + jplang + Jean-Philippe + Lang + jp_lang@yahoo.fr + 2007-09-28T00:16:04+02:00 + 2010-08-01T18:05:45+02:00 + 2011-08-01T18:05:45+02:00 + 2011-08-01T18:05:45+02:00 + ebc3f6b781a6fb3f2b0a83ce0ebb80e0d585189d + + 1 + + + + + + + + + + + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal(3, output.Id); + Assert.Equal("jplang", output.Login); + Assert.Equal("Jean-Philippe", output.FirstName); + Assert.Equal("Lang", output.LastName); + Assert.Equal("jp_lang@yahoo.fr", output.Email); + Assert.Equal(new DateTime(2007, 9, 28, 0, 16, 4, DateTimeKind.Local).AddHours(1), output.CreatedOn); + Assert.Equal(new DateTime(2010, 8, 1, 18, 5, 45, DateTimeKind.Local).AddHours(1), output.UpdatedOn); + Assert.Equal(new DateTime(2011, 8, 1, 18, 5, 45, DateTimeKind.Local).AddHours(1), output.LastLoginOn); + Assert.Equal(new DateTime(2011, 8, 1, 18, 5, 45, DateTimeKind.Local).AddHours(1), output.PasswordChangedOn); + Assert.Equal("ebc3f6b781a6fb3f2b0a83ce0ebb80e0d585189d", output.ApiKey); + Assert.Empty(output.AvatarUrl); + Assert.Equal(UserStatus.StatusActive, output.Status); + + var memberships = output.Memberships.ToList(); + Assert.Single(memberships); + Assert.Equal("Redmine", memberships[0].Project.Name); + Assert.Equal(1, memberships[0].Project.Id); + + var roles = memberships[0].Roles.ToList(); + Assert.Equal(2, roles.Count); + Assert.Equal("Administrator", roles[0].Name); + Assert.Equal(3, roles[0].Id); + Assert.Equal("Contributor", roles[1].Name); + Assert.Equal(4, roles[1].Id); + } + + [Fact] + public void Should_Deserialize_User_With_Groups() + { + const string input = """ + + + 3 + jplang + Jean-Philippe + Lang + jp_lang@yahoo.fr + 2007-09-28T00:16:04+02:00 + 2010-08-01T18:05:45+02:00 + 2011-08-01T18:05:45+02:00 + 2011-08-01T18:05:45+02:00 + ebc3f6b781a6fb3f2b0a83ce0ebb80e0d585189d + + 1 + + + + + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal(3, output.Id); + Assert.Equal("jplang", output.Login); + Assert.Equal("Jean-Philippe", output.FirstName); + Assert.Equal("Lang", output.LastName); + Assert.Equal("jp_lang@yahoo.fr", output.Email); + Assert.Equal(new DateTime(2007, 9, 28, 0, 16, 4, DateTimeKind.Local).AddHours(1), output.CreatedOn); + Assert.Equal(new DateTime(2010, 8, 1, 18, 5, 45, DateTimeKind.Local).AddHours(1), output.UpdatedOn); + Assert.Equal(new DateTime(2011, 8, 1, 18, 5, 45, DateTimeKind.Local).AddHours(1), output.LastLoginOn); + Assert.Equal(new DateTime(2011, 8, 1, 18, 5, 45, DateTimeKind.Local).AddHours(1), output.PasswordChangedOn); + Assert.Equal("ebc3f6b781a6fb3f2b0a83ce0ebb80e0d585189d", output.ApiKey); + Assert.Empty(output.AvatarUrl); + Assert.Equal(UserStatus.StatusActive, output.Status); + + var groups = output.Groups.ToList(); + Assert.Single(groups); + Assert.Equal("Developers", groups[0].Name); + Assert.Equal(20, groups[0].Id); + } +} + diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/VersionTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/VersionTests.cs new file mode 100644 index 00000000..28b7c3b2 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/VersionTests.cs @@ -0,0 +1,109 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class VersionTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Version() + { + const string input = """ + + + 2 + + 0.8 + + closed + 2008-12-30 + 0.0 + 0.0 + 2008-03-09T12:52:12+01:00 + 2009-11-15T12:22:12+01:00 + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal(2, output.Id); + Assert.Equal("Redmine", output.Project.Name); + Assert.Equal(1, output.Project.Id); + Assert.Equal("0.8", output.Name); + Assert.Equal(VersionStatus.Closed, output.Status); + Assert.Equal(new DateTime(2008, 12, 30), output.DueDate); + Assert.Equal(0.0f, output.EstimatedHours); + Assert.Equal(0.0f, output.SpentHours); + Assert.Equal(new DateTime(2008, 3, 9, 12, 52, 12, DateTimeKind.Local).AddHours(1), output.CreatedOn); + Assert.Equal(new DateTime(2009, 11, 15, 12, 22, 12, DateTimeKind.Local).AddHours(1), output.UpdatedOn); + + } + + [Fact] + public void Should_Deserialize_Versions() + { + const string input = """ + + + + 1 + + 0.7 + + closed + 2008-04-28 + none + 2008-03-09T12:52:06+01:00 + 2009-11-15T12:22:12+01:00 + FooBarWikiPage + + + 2 + + 0.8 + + closed + 2008-12-30 + none + FooBarWikiPage + 2008-03-09T12:52:12+01:00 + 2009-11-15T12:22:12+01:00 + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(34, output.TotalItems); + + var versions = output.Items.ToList(); + Assert.Equal(1, versions[0].Id); + Assert.Equal("Redmine", versions[0].Project.Name); + Assert.Equal(1, versions[0].Project.Id); + Assert.Equal("0.7", versions[0].Name); + Assert.Equal(VersionStatus.Closed, versions[0].Status); + Assert.Equal(new DateTime(2008, 4, 28), versions[0].DueDate); + Assert.Equal(VersionSharing.None, versions[0].Sharing); + Assert.Equal("FooBarWikiPage", versions[0].WikiPageTitle); + Assert.Equal(new DateTime(2008, 3, 9, 12, 52, 6, DateTimeKind.Local).AddHours(1), versions[0].CreatedOn); + Assert.Equal(new DateTime(2009, 11, 15, 12, 22, 12, DateTimeKind.Local).AddHours(1), versions[0].UpdatedOn); + + Assert.Equal(2, versions[1].Id); + Assert.Equal("Redmine", versions[1].Project.Name); + Assert.Equal(1, versions[1].Project.Id); + Assert.Equal("0.8", versions[1].Name); + Assert.Equal(VersionStatus.Closed, versions[1].Status); + Assert.Equal(new DateTime(2008, 12, 30), versions[1].DueDate); + Assert.Equal(VersionSharing.None, versions[1].Sharing); + Assert.Equal(new DateTime(2008, 3, 9, 12, 52, 12, DateTimeKind.Local).AddHours(1), versions[1].CreatedOn); + Assert.Equal(new DateTime(2009, 11, 15, 12, 22, 12, DateTimeKind.Local).AddHours(1), versions[1].UpdatedOn); + + } +} + + \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/WikiTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/WikiTests.cs new file mode 100644 index 00000000..a3dbdbc5 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/WikiTests.cs @@ -0,0 +1,133 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class WikiTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Wiki_Page() + { + const string input = """ + + + UsersGuide + + h1. Users Guide + ... + ... + 22 + + Typo + 2009-05-18T20:11:52Z + 2012-10-02T11:38:18Z + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal("UsersGuide", output.Title); + Assert.NotNull(output.ParentTitle); + Assert.Equal("Installation_Guide", output.ParentTitle); + + Assert.NotNull(output.Text); + Assert.False(string.IsNullOrWhiteSpace(output.Text), "Text should not be empty"); + + var lines = output.Text!.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + var firstLine = lines[0].Trim(); + + Assert.Equal("h1. Users Guide", firstLine); + + Assert.Equal(22, output.Version); + Assert.NotNull(output.Author); + Assert.Equal(11, output.Author.Id); + Assert.Equal("John Smith", output.Author.Name); + Assert.Equal("Typo", output.Comments); + Assert.Equal(new DateTime(2009, 5, 18, 20, 11, 52, DateTimeKind.Utc).ToLocalTime(), output.CreatedOn); + Assert.Equal(new DateTime(2012, 10, 2, 11, 38, 18, DateTimeKind.Utc).ToLocalTime(), output.UpdatedOn); + + } + + [Fact] + public void Should_Deserialize_Wiki_Pages() + { + const string input = """ + + + + UsersGuide + 2 + 2008-03-09T12:07:08Z + 2008-03-09T23:41:33+01:00 + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(1, output.TotalItems); + + var wikiPages = output.Items.ToList(); + Assert.Equal("UsersGuide", wikiPages[0].Title); + Assert.Equal(2, wikiPages[0].Version); + Assert.Equal(new DateTime(2008, 3, 9, 12, 7, 8, DateTimeKind.Utc).ToLocalTime(), wikiPages[0].CreatedOn); + Assert.Equal(new DateTime(2008, 3, 9, 22, 41, 33, DateTimeKind.Utc).ToLocalTime(), wikiPages[0].UpdatedOn); + } + + [Fact] + public void Should_Deserialize_Empty_Wiki_Pages() + { + const string input = """ + + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(0, output.TotalItems); + } + + [Fact] + public void Should_Deserialize_Wiki_With_Attachments() + { + const string input = """ + + + Te$t + QEcISExBVZ + 3 + + uAqCrmSBDUpNMOU + 2025-05-26T16:32:41Z + 2025-05-26T16:43:01Z + + + 155 + test-file_QPqCTEa + 512000 + text/plain + JIIMEcwtuZUsIHY + http://localhost:8089/attachments/download/155/test-file_QPqCTEa + + 2025-05-26T16:32:36Z + + + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Single(output.Attachments); + } +} + + + + \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/HostTests.cs b/tests/redmine-net-api.Tests/Tests/HostTests.cs new file mode 100644 index 00000000..dd4459bb --- /dev/null +++ b/tests/redmine-net-api.Tests/Tests/HostTests.cs @@ -0,0 +1,93 @@ +ο»Ώusing Padi.DotNet.RedmineAPI.Tests.Infrastructure.Order; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Options; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Tests +{ + [Trait("Redmine-api", "Host")] + [Order(1)] + public sealed class HostTests + { + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("localhost")] + [InlineData("http://")] + [InlineData("")] + [InlineData("xyztuv")] + [InlineData("ftp://example.com")] + [InlineData("ftp://localhost:3000")] + [InlineData("\"https://localhost:3000/"")] + [InlineData("C:/test/path/file.txt")] + [InlineData(@"\\host\share\some\directory\name\")] + [InlineData("xyz:c:\abc")] + [InlineData("file:///C:/test/path/file.txt")] + [InlineData("file://server/filename.ext")] + [InlineData("ftp://myUrl/../..")] + [InlineData("ftp://myUrl/%2E%2E/%2E%2E")] + [InlineData("example--domain.com")] + [InlineData("-example.com")] + [InlineData("example.com-")] + [InlineData("example.com/-")] + [InlineData("invalid-host")] + public void Should_Throw_Redmine_Exception_When_Host_Is_Invalid(string host) + { + // Arrange + var optionsBuilder = new RedmineManagerOptionsBuilder().WithHost(host); + + // Act and Assert + Assert.Throws(() => optionsBuilder.Build()); + } + + [Theory] + [InlineData("192.168.0.1", "https://192.168.0.1/")] + [InlineData("127.0.0.1", "https://127.0.0.1/")] + [InlineData("localhost:3000", "https://localhost:3000/")] + [InlineData("localhost:3000/", "https://localhost:3000/")] + [InlineData("https://localhost:3000/", "https://localhost:3000/")] + [InlineData("example.com", "https://example.com/")] + [InlineData("www.example.com", "https://www.example.com/")] + [InlineData("www.domain.com/", "https://www.domain.com/")] + [InlineData("www.domain.com:3000", "https://www.domain.com:3000/")] + [InlineData("https://www.google.com/", "https://www.google.com/")] + [InlineData("http://example.com:8080/", "http://example.com:8080/")] + [InlineData("http://example.com/path", "http://example.com/path")] + [InlineData("http://example.com/?param=value", "http://example.com/")] + [InlineData("http://example.com/#fragment", "http://example.com/")] + [InlineData("http://example.com/", "http://example.com/")] + [InlineData("http://example.com/?param=value", "http://example.com/")] + [InlineData("http://example.com/#fragment", "http://example.com/")] + [InlineData("http://example.com/path/page", "http://example.com/path/page")] + [InlineData("http://example.com/path/page?param=value", "http://example.com/path/page")] + [InlineData("http://example.com/path/page#fragment","http://example.com/path/page")] + [InlineData("http://[::1]:8080/", "http://[::1]/")] + [InlineData("http://www.domain.com/title/index.htm", "http://www.domain.com/")] + [InlineData("http://www.localhost.com/", "http://www.localhost.com/")] + [InlineData("https://www.localhost.com/", "https://www.localhost.com/")] + [InlineData("http://www.domain.com/", "http://www.domain.com/")] + [InlineData("http://www.domain.com/catalog/shownew.htm?date=today", "http://www.domain.com/")] + [InlineData("HTTP://www.domain.com:80//thick%20and%20thin.htm", "http://www.domain.com/")] + [InlineData("http://www.domain.com/index.htm#search", "http://www.domain.com/")] + [InlineData("http://www.domain.com:8080/", "http://www.domain.com:8080/")] + [InlineData("https://www.domain.com:8080/", "https://www.domain.com:8080/")] + [InlineData("http://[fe80::200:39ff:fe36:1a2d%254]/", "http://[fe80::200:39ff:fe36:1a2d]/")] + [InlineData("http://myurl/", "http://myurl/")] + [InlineData("http://[fe80::200:39ff:fe36:1a2d%254]/temp/example.htm", "http://[fe80::200:39ff:fe36:1a2d]/")] + [InlineData("http://myurl/", "http://myurl/")] + [InlineData("http://user:password@www.localhost.com/index.htm", "http://www.localhost.com/")] + public void Should_Not_Throw_Redmine_Exception_When_Host_Is_Valid(string host, string expected) + { + // Arrange + var optionsBuilder = new RedmineManagerOptionsBuilder().WithHost(host); + + // Act + var options = optionsBuilder.Build(); + + // Assert + Assert.NotNull(options); + Assert.Equal(expected, options.BaseAddress.ToString()); + } + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs b/tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs new file mode 100644 index 00000000..8f030881 --- /dev/null +++ b/tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs @@ -0,0 +1,479 @@ +using System.Collections.Specialized; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Net.Internal; +using Redmine.Net.Api.Types; +using Xunit; +using File = Redmine.Net.Api.Types.File; +using Version = Redmine.Net.Api.Types.Version; + + +namespace Padi.DotNet.RedmineAPI.Tests.Tests; + +public class RedmineApiUrlsTests(RedmineApiUrlsFixture fixture) : IClassFixture +{ + private string GetUriWithFormat(string path) + { + return string.Format(path, fixture.Format); + } + + [Fact] + public void MyAccount_ReturnsCorrectUrl() + { + var result = fixture.Sut.MyAccount(); + Assert.Equal(GetUriWithFormat("my/account.{0}"), result); + } + + [Theory] + [InlineData("123", "456", "issues/123/watchers/456.{0}")] + public void IssueWatcherRemove_WithValidIds_ReturnsCorrectUrl(string issueId, string userId, string expected) + { + var result = fixture.Sut.IssueWatcherRemove(issueId, userId); + Assert.Equal(GetUriWithFormat(expected), result); + } + + [Theory] + [InlineData("test.txt", "uploads.{0}?filename=test.txt")] + [InlineData("file with spaces.pdf", "uploads.{0}?filename=file%20with%20spaces.pdf")] + public void UploadFragment_WithFileName_ReturnsCorrectlyEncodedUrl(string fileName, string expected) + { + var result = fixture.Sut.UploadFragment(fileName); + Assert.Equal(GetUriWithFormat(expected), result); + } + + [Theory] + [InlineData("project1", "versions")] + [InlineData("project1", "issue_categories")] + public void ProjectParentFragment_ForDifferentTypes_ReturnsCorrectUrl(string projectId, string fragment) + { + var expected = $"projects/{projectId}/{fragment}.{{0}}"; + var result = fixture.Sut.ProjectParentFragment(projectId, fragment); + Assert.Equal(GetUriWithFormat(expected), result); + } + + [Theory] + [InlineData("issue1", "relations")] + [InlineData("issue1", "watchers")] + public void IssueParentFragment_ForDifferentTypes_ReturnsCorrectUrl(string issueId, string fragment) + { + var expected = $"issues/{issueId}/{fragment}.{{0}}"; + var result = fixture.Sut.IssueParentFragment(issueId, fragment); + Assert.Equal(GetUriWithFormat(expected), result); + } + + [Theory] + [MemberData(nameof(GetFragmentTestData))] + public void GetFragment_ForAllTypes_ReturnsCorrectUrl(Type type, string id, string expected) + { + var result = fixture.Sut.GetFragment(type, id); + Assert.Equal(GetUriWithFormat(expected), result); + } + + [Theory] + [MemberData(nameof(CreateEntityTestData))] + public void CreateEntity_ForAllTypes_ReturnsCorrectUrl(Type type, string ownerId, string expected) + { + var result = fixture.Sut.CreateEntityFragment(type, ownerId); + Assert.Equal(GetUriWithFormat(expected), result); + } + + [Theory] + [MemberData(nameof(GetListTestData))] + public void GetList_ForAllTypes_ReturnsCorrectUrl(Type type, string ownerId, string expected) + { + var result = fixture.Sut.GetListFragment(type, ownerId); + Assert.Equal(GetUriWithFormat(expected), result); + } + + [Theory] + [MemberData(nameof(InvalidTypeTestData))] + public void GetList_WithInvalidType_ThrowRedmineException(Type invalidType) + { + var exception = Assert.Throws(() => fixture.Sut.GetListFragment(invalidType)); + Assert.Contains("There is no uri fragment defined for type", exception.Message); + } + + [Theory] + [MemberData(nameof(GetListWithIssueIdTestData))] + public void GetListFragment_WithIssueIdInRequestOptions_ReturnsCorrectUrl(Type type, string issueId, string expected) + { + var requestOptions = new RequestOptions + { + QueryString = new NameValueCollection + { + { RedmineKeys.ISSUE_ID, issueId } + } + }; + + var result = fixture.Sut.GetListFragment(type, requestOptions); + Assert.Equal(GetUriWithFormat(expected), result); + } + + [Theory] + [MemberData(nameof(GetListWithProjectIdTestData))] + public void GetListFragment_WithProjectIdInRequestOptions_ReturnsCorrectUrl(Type type, string projectId, string expected) + { + var requestOptions = new RequestOptions + { + QueryString = new NameValueCollection + { + { RedmineKeys.PROJECT_ID, projectId } + } + }; + + var result = fixture.Sut.GetListFragment(type, requestOptions); + Assert.Equal(GetUriWithFormat(expected), result); + } + + [Theory] + [MemberData(nameof(GetListWithBothIdsTestData))] + public void GetListFragment_WithBothIds_PrioritizesProjectId(Type type, string projectId, string issueId, string expected) + { + var requestOptions = new RequestOptions + { + QueryString = new NameValueCollection + { + { RedmineKeys.PROJECT_ID, projectId }, + { RedmineKeys.ISSUE_ID, issueId } + } + }; + + var result = fixture.Sut.GetListFragment(type, requestOptions); + Assert.Equal(GetUriWithFormat(expected), result); + } + + [Theory] + [MemberData(nameof(GetListWithNoIdsTestData))] + public void GetListFragment_WithNoIds_ReturnsDefaultUrl(Type type, string expected) + { + var result = fixture.Sut.GetListFragment(type, new RequestOptions()); + Assert.Equal(GetUriWithFormat(expected), result); + } + + [Theory] + [ClassData(typeof(RedmineTypeTestData))] + public void GetListFragment_ForAllTypes_ReturnsCorrectUrl(Type type, string parentId, string expected) + { + var result = fixture.Sut.GetListFragment(type, parentId); + Assert.Equal(GetUriWithFormat(expected), result); + } + + [Theory] + [MemberData(nameof(GetListEntityRequestOptionTestData))] + public void GetListFragment_WithEmptyOptions_ReturnsCorrectUrl(Type type, RequestOptions requestOptions, string expected) + { + var result = fixture.Sut.GetListFragment(type, requestOptions); + Assert.Equal(GetUriWithFormat(expected), result); + } + + [Theory] + [ClassData(typeof(RedmineTypeTestData))] + public void GetListFragment_WithNullOptions_ReturnsCorrectUrl(Type type, string parentId, string expected) + { + var result = fixture.Sut.GetListFragment(type, parentId); + Assert.Equal(GetUriWithFormat(expected), result); + } + + [Theory] + [MemberData(nameof(GetListWithNullRequestOptionsTestData))] + public void GetListFragment_WithNullRequestOptions_ReturnsDefaultUrl(Type type, string expected) + { + var result = fixture.Sut.GetListFragment(type, (RequestOptions)null); + Assert.Equal(GetUriWithFormat(expected), result); + } + + [Theory] + [MemberData(nameof(GetListWithEmptyQueryStringTestData))] + public void GetListFragment_WithEmptyQueryString_ReturnsDefaultUrl(Type type, string expected) + { + var requestOptions = new RequestOptions + { + QueryString = null + }; + + var result = fixture.Sut.GetListFragment(type, requestOptions); + Assert.Equal(GetUriWithFormat(expected), result); + } + + [Fact] + public void GetListFragment_WithCustomQueryParameters_DoesNotAffectUrl() + { + var requestOptions = new RequestOptions + { + QueryString = new NameValueCollection + { + { "status_id", "1" }, + { "assigned_to_id", "me" }, + { "sort", "priority:desc" } + } + }; + + var result = fixture.Sut.GetListFragment(requestOptions); + Assert.Equal(GetUriWithFormat("issues.{0}"), result); + } + + [Theory] + [MemberData(nameof(GetListWithInvalidTypeTestData))] + public void GetListFragment_WithInvalidType_ThrowsRedmineException(Type invalidType) + { + var exception = Assert.Throws(() => fixture.Sut.GetListFragment(invalidType)); + + Assert.Contains("There is no uri fragment defined for type", exception.Message); + } + + public static TheoryData GetListWithBothIdsTestData() + { + return new TheoryData + { + { + typeof(Version), + "project1", + "issue1", + "projects/project1/versions.{0}" + }, + { + typeof(IssueCategory), + "project2", + "issue2", + "projects/project2/issue_categories.{0}" + } + }; + } + + public class RedmineTypeTestData : TheoryData + { + public RedmineTypeTestData() + { + Add(null, "issues.{0}"); + Add(null,"projects.{0}"); + Add(null,"users.{0}"); + Add(null,"time_entries.{0}"); + Add(null,"custom_fields.{0}"); + Add(null,"groups.{0}"); + Add(null,"news.{0}"); + Add(null,"queries.{0}"); + Add(null,"roles.{0}"); + Add(null,"issue_statuses.{0}"); + Add(null,"trackers.{0}"); + Add(null,"enumerations/issue_priorities.{0}"); + Add(null,"enumerations/time_entry_activities.{0}"); + Add("1","projects/1/versions.{0}"); + Add("1","projects/1/issue_categories.{0}"); + Add("1","projects/1/memberships.{0}"); + Add("1","issues/1/relations.{0}"); + Add(null,"attachments.{0}"); + Add(null,"custom_fields.{0}"); + Add(null,"journals.{0}"); + Add(null,"search.{0}"); + Add(null,"watchers.{0}"); + } + + private void Add(string parentId, string expected) where T : class, new() + { + AddRow(typeof(T), parentId, expected); + } + } + + public static TheoryData GetFragmentTestData() + { + return new TheoryData + { + { typeof(Attachment), "1", "attachments/1.{0}" }, + { typeof(CustomField), "2", "custom_fields/2.{0}" }, + { typeof(Group), "3", "groups/3.{0}" }, + { typeof(Issue), "4", "issues/4.{0}" }, + { typeof(IssueCategory), "5", "issue_categories/5.{0}" }, + { typeof(IssueCustomField), "6", "custom_fields/6.{0}" }, + { typeof(IssuePriority), "7", "enumerations/issue_priorities/7.{0}" }, + { typeof(IssueRelation), "8", "relations/8.{0}" }, + { typeof(IssueStatus), "9", "issue_statuses/9.{0}" }, + { typeof(Journal), "10", "journals/10.{0}" }, + { typeof(News), "11", "news/11.{0}" }, + { typeof(Project), "12", "projects/12.{0}" }, + { typeof(ProjectMembership), "13", "memberships/13.{0}" }, + { typeof(Query), "14", "queries/14.{0}" }, + { typeof(Role), "15", "roles/15.{0}" }, + { typeof(Search), "16", "search/16.{0}" }, + { typeof(TimeEntry), "17", "time_entries/17.{0}" }, + { typeof(TimeEntryActivity), "18", "enumerations/time_entry_activities/18.{0}" }, + { typeof(Tracker), "19", "trackers/19.{0}" }, + { typeof(User), "20", "users/20.{0}" }, + { typeof(Version), "21", "versions/21.{0}" }, + { typeof(Watcher), "22", "watchers/22.{0}" } + }; + } + + public static TheoryData CreateEntityTestData() + { + return new TheoryData + { + { typeof(Version), "project1", "projects/project1/versions.{0}" }, + { typeof(IssueCategory), "project1", "projects/project1/issue_categories.{0}" }, + { typeof(ProjectMembership), "project1", "projects/project1/memberships.{0}" }, + + { typeof(IssueRelation), "issue1", "issues/issue1/relations.{0}" }, + + { typeof(File), "project1", "projects/project1/files.{0}" }, + { typeof(Upload), null, "uploads.{0}" }, + { typeof(Attachment), "issue1", "/attachments/issues/issue1.{0}" }, + + { typeof(Issue), null, "issues.{0}" }, + { typeof(Project), null, "projects.{0}" }, + { typeof(User), null, "users.{0}" }, + { typeof(TimeEntry), null, "time_entries.{0}" }, + { typeof(News), null, "news.{0}" }, + { typeof(Query), null, "queries.{0}" }, + { typeof(Role), null, "roles.{0}" }, + { typeof(Group), null, "groups.{0}" }, + { typeof(CustomField), null, "custom_fields.{0}" }, + { typeof(IssueStatus), null, "issue_statuses.{0}" }, + { typeof(Tracker), null, "trackers.{0}" }, + { typeof(IssuePriority), null, "enumerations/issue_priorities.{0}" }, + { typeof(TimeEntryActivity), null, "enumerations/time_entry_activities.{0}" } + }; + } + + public static TheoryData GetListEntityRequestOptionTestData() + { + var rqWithProjectId = new RequestOptions() + { + QueryString = new NameValueCollection() + { + {RedmineKeys.PROJECT_ID, "project1"} + } + }; + var rqWithPIssueId = new RequestOptions() + { + QueryString = new NameValueCollection() + { + {RedmineKeys.ISSUE_ID, "issue1"} + } + }; + return new TheoryData + { + { typeof(Version), rqWithProjectId, "projects/project1/versions.{0}" }, + { typeof(IssueCategory), rqWithProjectId, "projects/project1/issue_categories.{0}" }, + { typeof(ProjectMembership), rqWithProjectId, "projects/project1/memberships.{0}" }, + + { typeof(IssueRelation), rqWithPIssueId, "issues/issue1/relations.{0}" }, + + { typeof(File), rqWithProjectId, "projects/project1/files.{0}" }, + { typeof(Attachment), rqWithPIssueId, "attachments.{0}" }, + + { typeof(Issue), null, "issues.{0}" }, + { typeof(Project), null, "projects.{0}" }, + { typeof(User), null, "users.{0}" }, + { typeof(TimeEntry), null, "time_entries.{0}" }, + { typeof(News), null, "news.{0}" }, + { typeof(Query), null, "queries.{0}" }, + { typeof(Role), null, "roles.{0}" }, + { typeof(Group), null, "groups.{0}" }, + { typeof(CustomField), null, "custom_fields.{0}" }, + { typeof(IssueStatus), null, "issue_statuses.{0}" }, + { typeof(Tracker), null, "trackers.{0}" }, + { typeof(IssuePriority), null, "enumerations/issue_priorities.{0}" }, + { typeof(TimeEntryActivity), null, "enumerations/time_entry_activities.{0}" } + }; + } + + public static TheoryData GetListTestData() + { + return new TheoryData + { + { typeof(Version), "project1", "projects/project1/versions.{0}" }, + { typeof(IssueCategory), "project1", "projects/project1/issue_categories.{0}" }, + { typeof(ProjectMembership), "project1", "projects/project1/memberships.{0}" }, + + { typeof(IssueRelation), "issue1", "issues/issue1/relations.{0}" }, + + { typeof(File), "project1", "projects/project1/files.{0}" }, + + { typeof(Issue), null, "issues.{0}" }, + { typeof(Project), null, "projects.{0}" }, + { typeof(User), null, "users.{0}" }, + { typeof(TimeEntry), null, "time_entries.{0}" }, + { typeof(News), null, "news.{0}" }, + { typeof(Query), null, "queries.{0}" }, + { typeof(Role), null, "roles.{0}" }, + { typeof(Group), null, "groups.{0}" }, + { typeof(CustomField), null, "custom_fields.{0}" }, + { typeof(IssueStatus), null, "issue_statuses.{0}" }, + { typeof(Tracker), null, "trackers.{0}" }, + { typeof(IssuePriority), null, "enumerations/issue_priorities.{0}" }, + { typeof(TimeEntryActivity), null, "enumerations/time_entry_activities.{0}" } + }; + } + + public static TheoryData GetListWithIssueIdTestData() + { + return new TheoryData + { + { typeof(IssueRelation), "issue1", "issues/issue1/relations.{0}" }, + }; + } + + public static TheoryData GetListWithProjectIdTestData() + { + return new TheoryData + { + { typeof(Version), "1", "projects/1/versions.{0}" }, + { typeof(IssueCategory), "1", "projects/1/issue_categories.{0}" }, + { typeof(ProjectMembership), "1", "projects/1/memberships.{0}" }, + { typeof(File), "1", "projects/1/files.{0}" }, + }; + } + + public static TheoryData GetListWithNullRequestOptionsTestData() + { + return new TheoryData + { + { typeof(Issue), "issues.{0}" }, + { typeof(Project), "projects.{0}" }, + { typeof(User), "users.{0}" } + }; + } + + public static TheoryData GetListWithEmptyQueryStringTestData() + { + return new TheoryData + { + { typeof(Issue), "issues.{0}" }, + { typeof(Project), "projects.{0}" }, + { typeof(User), "users.{0}" } + }; + } + + public static TheoryData GetListWithInvalidTypeTestData() + { + return + [ + typeof(string), + typeof(int), + typeof(DateTime), + typeof(object) + ]; + } + + public static TheoryData GetListWithNoIdsTestData() + { + return new TheoryData + { + { typeof(Issue), "issues.{0}" }, + { typeof(Project), "projects.{0}" }, + { typeof(User), "users.{0}" }, + { typeof(TimeEntry), "time_entries.{0}" }, + { typeof(CustomField), "custom_fields.{0}" } + }; + } + + public static TheoryData InvalidTypeTestData() + { + return + [ + typeof(object), + typeof(int) + ]; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj new file mode 100644 index 00000000..0eb2e805 --- /dev/null +++ b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj @@ -0,0 +1,56 @@ + + + + + + Padi.DotNet.RedmineAPI.Tests + disable + enable + $(AssemblyName) + false + net481 + false + f8b9e946-b547-42f1-861c-f719dca00a84 + Release;Debug;DebugJson + + + + |net40|net45|net451|net452|net46|net461| + |net45|net451|net452|net46|net461| + |net40|net45|net451|net452|net46|net461|net462|net470|net471|net472|net48|net481| + |net45|net451|net452|net46|net461|net462|net470|net471|net472|net48|net481| + + + + DEBUG;TRACE;DEBUG_XML + + + + DEBUG;TRACE;DEBUG_JSON + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + \ No newline at end of file diff --git a/version.props b/version.props new file mode 100644 index 00000000..05e950d9 --- /dev/null +++ b/version.props @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Helper.cs b/xUnitTest-redmine-net45-api/Helper.cs deleted file mode 100755 index a29665ee..00000000 --- a/xUnitTest-redmine-net45-api/Helper.cs +++ /dev/null @@ -1,25 +0,0 @@ -ο»Ώusing System.Configuration; - -namespace xUnitTestredminenet45api -{ - internal static class Helper - { - public static string Uri { get; private set; } - - public static string ApiKey { get; private set; } - - public static string Username { get; private set; } - - public static string Password { get; private set; } - - static Helper() - { - Uri = ConfigurationManager.AppSettings["uri"]; - ApiKey = ConfigurationManager.AppSettings["apiKey"]; - - Username = ConfigurationManager.AppSettings["username"]; - Password = ConfigurationManager.AppSettings["password"]; - } - } -} - diff --git a/xUnitTest-redmine-net45-api/Infrastructure/RedmineCollection.cs b/xUnitTest-redmine-net45-api/Infrastructure/RedmineCollection.cs deleted file mode 100755 index d65eae0c..00000000 --- a/xUnitTest-redmine-net45-api/Infrastructure/RedmineCollection.cs +++ /dev/null @@ -1,10 +0,0 @@ -ο»Ώusing Xunit; - -namespace xUnitTestredminenet45api -{ - [CollectionDefinition("RedmineCollection")] - public class RedmineCollection : ICollectionFixture - { - - } -} \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Properties/AssemblyInfo.cs b/xUnitTest-redmine-net45-api/Properties/AssemblyInfo.cs deleted file mode 100755 index 4c8b96f1..00000000 --- a/xUnitTest-redmine-net45-api/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,34 +0,0 @@ -ο»Ώusing System.Reflection; -using xUnitTestredminenet45api; -using Xunit; - -// Information about this assembly is defined by the following attributes. -// Change them to the values specific to your project. - -[assembly: AssemblyTitle("xUnitTest-redmine-net45-api")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("")] -[assembly: AssemblyCopyright("Copyright Β© 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". -// The form "{Major}.{Minor}.*" will automatically update the build and revision, -// and "{Major}.{Minor}.{Build}.*" will update just the revision. - -[assembly: AssemblyVersion("1.0.*")] - -// The following attributes are used to specify the signing key for the assembly, -// if desired. See the Mono documentation for more information about signing. - -//[assembly: AssemblyDelaySign(false)] -//[assembly: AssemblyKeyFile("")] - - - -[assembly: TestCaseOrderer(CaseOrderer.TYPE_NAME, CaseOrderer.ASSEMBY_NAME)] -[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly, DisableTestParallelization = true)] -[assembly: TestCollectionOrderer(CollectionOrderer.TYPE_NAME, CollectionOrderer.ASSEMBY_NAME)] - diff --git a/xUnitTest-redmine-net45-api/RedmineFixture.cs b/xUnitTest-redmine-net45-api/RedmineFixture.cs deleted file mode 100755 index c707b38d..00000000 --- a/xUnitTest-redmine-net45-api/RedmineFixture.cs +++ /dev/null @@ -1,28 +0,0 @@ -ο»Ώusing System.Diagnostics; -using Redmine.Net.Api; - -namespace xUnitTestredminenet45api -{ - public class RedmineFixture - { - public RedmineManager RedmineManager { get; set; } - - public RedmineFixture () - { - SetMimeTypeXML(); - SetMimeTypeJSON(); - } - - [Conditional("JSON")] - private void SetMimeTypeJSON() - { - RedmineManager = new RedmineManager(Helper.Uri, Helper.ApiKey, MimeFormat.Json); - } - - [Conditional("XML")] - private void SetMimeTypeXML() - { - RedmineManager = new RedmineManager(Helper.Uri, Helper.ApiKey); - } - } -} \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Tests/Async/AttachmentAsyncTests.cs b/xUnitTest-redmine-net45-api/Tests/Async/AttachmentAsyncTests.cs deleted file mode 100755 index 83d536e0..00000000 --- a/xUnitTest-redmine-net45-api/Tests/Async/AttachmentAsyncTests.cs +++ /dev/null @@ -1,95 +0,0 @@ -ο»Ώusing System; -using System.IO; -using System.Collections.Specialized; -using System.Collections.Generic; -using Xunit; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Async; -using System.Threading.Tasks; - -namespace xUnitTestredminenet45api -{ - [Collection("RedmineCollection")] - public class AttachmentAsyncTests - { - private const string ATTACHMENT_ID = "10"; - - private readonly RedmineFixture fixture; - public AttachmentAsyncTests (RedmineFixture fixture) - { - this.fixture = fixture; - } - - [Fact] - public async Task Should_Get_Attachment_By_Id() - { - var attachment = await fixture.RedmineManager.GetObjectAsync(ATTACHMENT_ID, null); - - Assert.NotNull(attachment); - Assert.IsType(attachment); - } - - [Fact] - public async Task Should_Upload_Attachment() - { - //read document from specified path - string documentPath = AppDomain.CurrentDomain.BaseDirectory+ "/uploadAttachment.pages"; - byte[] documentData = System.IO.File.ReadAllBytes(documentPath); - - //upload attachment to redmine - Upload attachment = await fixture.RedmineManager.UploadFileAsync(documentData); - - //set attachment properties - attachment.FileName = "uploadAttachment.pages"; - attachment.Description = "File uploaded using REST API"; - attachment.ContentType = "text/plain"; - - //create list of attachments to be added to issue - IList attachments = new List(); - attachments.Add(attachment); - - Issue issue = new Issue(); - issue.Project = new Project { Id = 9 }; - issue.Tracker = new IdentifiableName { Id = 3 }; - issue.Status = new IdentifiableName { Id = 6 }; - issue.Priority = new IdentifiableName { Id = 9 }; - issue.Subject = "Issue with attachments"; - issue.Description = "Issue description..."; - issue.Category = new IdentifiableName { Id = 18 }; - issue.FixedVersion = new IdentifiableName { Id = 9 }; - issue.AssignedTo = new IdentifiableName { Id = 8 }; - issue.ParentIssue = new IdentifiableName { Id = 96 }; - issue.CustomFields = new List(); - issue.CustomFields.Add(new IssueCustomField { Id = 13, Values = new List { new CustomFieldValue { Info = "Issue custom field completed" } } }); - issue.IsPrivate = true; - issue.EstimatedHours = 12; - issue.StartDate = DateTime.Now; - issue.DueDate = DateTime.Now.AddMonths(1); - issue.Uploads = attachments; - issue.Watchers = new List(); - issue.Watchers.Add(new Watcher { Id = 8 }); - issue.Watchers.Add(new Watcher { Id = 2 }); - - //create issue and attach document - Issue issueWithAttachment = await fixture.RedmineManager.CreateObjectAsync(issue); - - issue = await fixture.RedmineManager.GetObjectAsync(issueWithAttachment.Id.ToString(), new NameValueCollection { { "include", "attachments" } }); - - Assert.NotNull(issue); - Assert.IsType(issue); - - Assert.True(issue.Attachments.Count == 1, "Attachments count != 1"); - Assert.True(issue.Attachments[0].FileName == attachment.FileName); - } - - [Fact] - public async Task Sould_Download_Attachment() - { - var attachment = await fixture.RedmineManager.GetObjectAsync(ATTACHMENT_ID, null); - - var document = await fixture.RedmineManager.DownloadFileAsync(attachment.ContentUrl); - - Assert.NotNull(document); - } - } -} \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Tests/Async/IssueAsyncTests.cs b/xUnitTest-redmine-net45-api/Tests/Async/IssueAsyncTests.cs deleted file mode 100755 index bd447e7a..00000000 --- a/xUnitTest-redmine-net45-api/Tests/Async/IssueAsyncTests.cs +++ /dev/null @@ -1,44 +0,0 @@ -ο»Ώusing Xunit; -using Redmine.Net.Api.Async; -using Redmine.Net.Api.Types; -using System.Threading.Tasks; -using System.Collections.Specialized; -using System.Collections.Generic; - -namespace xUnitTestredminenet45api -{ - [Collection("RedmineCollection")] - public class IssueAsyncTests - { - private const int WATCHER_ISSUE_ID = 91; - private const int WATCHER_USER_ID = 2; - - private readonly RedmineFixture fixture; - public IssueAsyncTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - [Fact] - public async Task Should_Add_Watcher_To_Issue() - { - await fixture.RedmineManager.AddWatcherAsync(WATCHER_ISSUE_ID, WATCHER_USER_ID); - - Issue issue = await fixture.RedmineManager.GetObjectAsync(WATCHER_ISSUE_ID.ToString(), new NameValueCollection { { "include", "watchers" } }); - - Assert.NotNull(issue); - Assert.True(issue.Watchers.Count == 1, "Number of watchers != 1"); - Assert.True(((List)issue.Watchers).Find(w => w.Id == WATCHER_USER_ID) != null, "Watcher not added to issue."); - } - - [Fact] - public async Task Should_Remove_Watcher_From_Issue() - { - await fixture.RedmineManager.RemoveWatcherAsync(WATCHER_ISSUE_ID, WATCHER_USER_ID); - - Issue issue = await fixture.RedmineManager.GetObjectAsync(WATCHER_ISSUE_ID.ToString(), new NameValueCollection { { "include", "watchers" } }); - - Assert.True(issue.Watchers == null || ((List)issue.Watchers).Find(w => w.Id == WATCHER_USER_ID) == null); - } - } -} \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Tests/Async/UserAsyncTests.cs b/xUnitTest-redmine-net45-api/Tests/Async/UserAsyncTests.cs deleted file mode 100755 index 4c2f3203..00000000 --- a/xUnitTest-redmine-net45-api/Tests/Async/UserAsyncTests.cs +++ /dev/null @@ -1,207 +0,0 @@ -ο»Ώusing System; -using Xunit; -using Redmine.Net.Api; -using Redmine.Net.Api.Async; -using Redmine.Net.Api.Types; -using System.Threading.Tasks; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; -using System.Linq; -using Redmine.Net.Api.Exceptions; - -namespace xUnitTestredminenet45api -{ - [Collection("RedmineCollection")] - public class UserAsyncTests - { - private const string USER_ID = "8"; - private const string LIMIT = "2"; - private const string OFFSET = "1"; - private const int GROUP_ID = 9; - - private readonly RedmineFixture fixture; - public UserAsyncTests (RedmineFixture fixture) - { - this.fixture = fixture; - } - - [Fact] - public async Task Should_Get_CurrentUser() - { - var currentUser = await fixture.RedmineManager.GetCurrentUserAsync(); - Assert.NotNull(currentUser); - } - - [Fact] - public async Task Should_Get_User_By_Id() - { - var user = await fixture.RedmineManager.GetObjectAsync(USER_ID, null); - Assert.NotNull(user); - } - - [Fact] - public async Task Should_Get_User_By_Id_Including_Groups_And_Memberships() - { - var user = await fixture.RedmineManager.GetObjectAsync(USER_ID, new NameValueCollection() { { RedmineKeys.INCLUDE, "groups,memberships" } }); - - Assert.NotNull(user); - - Assert.NotNull (user.Groups); - Assert.True(user.Groups.Count == 1, "Group count != 1"); - - Assert.NotNull (user.Memberships); - Assert.True(user.Memberships.Count == 3, "Membership count != 3"); - } - - [Fact] - public async Task Should_Get_X_Users_From_Offset_Y() - { - var result = await fixture.RedmineManager.GetPaginatedObjectsAsync(new NameValueCollection() { - { RedmineKeys.INCLUDE, "groups, memberships" }, - {RedmineKeys.LIMIT,LIMIT }, - {RedmineKeys.OFFSET,OFFSET } - }); - - Assert.NotNull(result); - Assert.All (result.Objects, u => Assert.IsType (u)); - } - - [Fact] - public async Task Should_Get_All_Users_With_Groups_And_Memberships() - { - List users = await fixture.RedmineManager.GetObjectsAsync(new NameValueCollection { { RedmineKeys.INCLUDE, "groups, memberships" } }); - - Assert.NotNull(users); - Assert.All (users, u => Assert.IsType (u)); - } - - [Fact] - public async Task Should_Get_Active_Users() - { - var users = await fixture.RedmineManager.GetObjectsAsync(new NameValueCollection() - { - { RedmineKeys.STATUS, ((int)UserStatus.STATUS_ACTIVE).ToString(CultureInfo.InvariantCulture) } - }); - - Assert.NotNull(users); - Assert.True(users.Count == 6); - Assert.All (users, u => Assert.IsType (u)); - } - - [Fact] - public async Task Should_Get_Anonymous_Users() - { - var users = await fixture.RedmineManager.GetObjectsAsync(new NameValueCollection() - { - { RedmineKeys.STATUS, ((int)UserStatus.STATUS_ANONYMOUS).ToString(CultureInfo.InvariantCulture) } - }); - - Assert.NotNull(users); - Assert.True(users.Count == 0); - Assert.All (users, u => Assert.IsType (u)); - } - - [Fact] - public async Task Should_Get_Locked_Users() - { - var users = await fixture.RedmineManager.GetObjectsAsync(new NameValueCollection() - { - { RedmineKeys.STATUS, ((int)UserStatus.STATUS_LOCKED).ToString(CultureInfo.InvariantCulture) } - }); - - Assert.NotNull(users); - Assert.True(users.Count == 1); - Assert.All (users, u => Assert.IsType (u)); - } - - [Fact] - public async Task Should_Get_Registered_Users() - { - var users = await fixture.RedmineManager.GetObjectsAsync(new NameValueCollection() - { - { RedmineKeys.STATUS, ((int)UserStatus.STATUS_REGISTERED).ToString(CultureInfo.InvariantCulture) } - }); - - Assert.NotNull(users); - Assert.True(users.Count == 1); - Assert.All (users, u => Assert.IsType (u)); - } - - [Fact] - public async Task Should_Get_Users_By_Group() - { - var users = await fixture.RedmineManager.GetObjectsAsync(new NameValueCollection() - { - {RedmineKeys.GROUP_ID, GROUP_ID.ToString(CultureInfo.InvariantCulture)} - }); - - Assert.NotNull(users); - Assert.True(users.Count == 3); - Assert.All (users, u => Assert.IsType (u)); - } - - [Fact] - public async Task Should_Add_User_To_Group() - { - await fixture.RedmineManager.AddUserToGroupAsync(GROUP_ID, int.Parse(USER_ID)); - - User user = fixture.RedmineManager.GetObject(USER_ID.ToString(CultureInfo.InvariantCulture), new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.GROUPS } }); - - Assert.NotNull (user.Groups); - Assert.True(user.Groups.FirstOrDefault(g => g.Id == GROUP_ID) != null); - } - - [Fact] - public async Task Should_Remove_User_From_Group() - { - await fixture.RedmineManager.DeleteUserFromGroupAsync(GROUP_ID, int.Parse(USER_ID)); - - User user = await fixture.RedmineManager.GetObjectAsync(USER_ID.ToString(CultureInfo.InvariantCulture), new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.GROUPS } }); - - Assert.True(user.Groups == null || user.Groups.FirstOrDefault(g => g.Id == GROUP_ID) == null); - } - - [Fact] - public async Task Should_Create_User() - { - User user = new User(); - user.Login = "userTestLogin4"; - user.FirstName = "userTestFirstName"; - user.LastName = "userTestLastName"; - user.Email = "testTest4@redmineapi.com"; - user.Password = "123456"; - user.AuthenticationModeId = 1; - user.MustChangePassword = false; - user.CustomFields = new List(); - user.CustomFields.Add(new IssueCustomField { Id = 4, Values = new List { new CustomFieldValue { Info = "userTestCustomField:" + DateTime.UtcNow } } }); - - var createdUser = await fixture.RedmineManager.CreateObjectAsync(user); - - Assert.Equal(user.Login, createdUser.Login); - Assert.Equal(user.Email, createdUser.Email); - } - - [Fact] - public async Task Should_Update_User() - { - var userId = 59.ToString(); - User user = fixture.RedmineManager.GetObject(userId, null); - user.FirstName = "modified first name"; - await fixture.RedmineManager.UpdateObjectAsync(userId, user); - - User updatedUser = await fixture.RedmineManager.GetObjectAsync(userId, null); - - Assert.Equal(user.FirstName, updatedUser.FirstName); - } - - [Fact] - public async Task Should_Delete_User() - { - var userId = 62.ToString(); - await fixture.RedmineManager.DeleteObjectAsync(userId, null); - await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetObjectAsync(userId, null)); - - } - } -} \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Tests/Async/WikiPageAsyncTests.cs b/xUnitTest-redmine-net45-api/Tests/Async/WikiPageAsyncTests.cs deleted file mode 100755 index a2c651aa..00000000 --- a/xUnitTest-redmine-net45-api/Tests/Async/WikiPageAsyncTests.cs +++ /dev/null @@ -1,74 +0,0 @@ -ο»Ώusing Xunit; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Async; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Threading.Tasks; -using Redmine.Net.Api.Exceptions; - -namespace xUnitTestredminenet45api -{ - [Collection("RedmineCollection")] - public class WikiPageAsyncTests - { - private const string PROJECT_ID = "redmine-net-testq"; - private const string WIKI_PAGE_NAME = "Wiki"; - private const int NO_OF_WIKI_PAGES = 1; - private const int WIKI_PAGE_VERSION = 1; - - private const string WIKI_PAGE_UPDATED_TEXT = "Updated again and again wiki page"; - private const string WIKI_PAGE_COMMENT = "Comment added through code"; - - private readonly RedmineFixture fixture; - public WikiPageAsyncTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - [Fact] - public async Task Should_Add_Or_Update_Page() - { - WikiPage page = await fixture.RedmineManager.CreateOrUpdateWikiPageAsync(PROJECT_ID, WIKI_PAGE_NAME, new WikiPage { Text = WIKI_PAGE_UPDATED_TEXT, Comments = WIKI_PAGE_COMMENT }); - - Assert.NotNull(page); - Assert.True(page.Title == WIKI_PAGE_NAME, "Wiki page " + WIKI_PAGE_NAME + " does not exist."); - } - - [Fact] - public async Task Should_Get_All_Pages() - { - List pages = await fixture.RedmineManager.GetAllWikiPagesAsync(null, PROJECT_ID); - - Assert.NotNull(pages); - - Assert.True(pages.Count == NO_OF_WIKI_PAGES, "Number of pages != "+NO_OF_WIKI_PAGES); - Assert.True(pages.Exists(p => p.Title == WIKI_PAGE_NAME), "Wiki page "+WIKI_PAGE_NAME+" does not exist." ); - } - - [Fact] - public async Task Should_Get_Page_By_Name() - { - WikiPage page = await fixture.RedmineManager.GetWikiPageAsync(PROJECT_ID, new NameValueCollection { { "include", "attachments" } }, WIKI_PAGE_NAME); - - Assert.NotNull(page); - Assert.True(page.Title == WIKI_PAGE_NAME, "Wiki page " + WIKI_PAGE_NAME + " does not exist."); - } - - [Fact] - public async Task Should_Get_Wiki_Page_Old_Version() - { - WikiPage oldPage = await fixture.RedmineManager.GetWikiPageAsync(PROJECT_ID, new NameValueCollection { { "include", "attachments" } }, WIKI_PAGE_NAME, WIKI_PAGE_VERSION); - - Assert.True(oldPage.Title == WIKI_PAGE_NAME, "Wiki page " + WIKI_PAGE_NAME + " does not exist."); - Assert.True(oldPage.Version == WIKI_PAGE_VERSION, "Wiki page version " + WIKI_PAGE_VERSION + " does not exist."); - } - - [Fact] - public async Task Should_Delete_WikiPage() - { - await fixture.RedmineManager.DeleteWikiPageAsync(PROJECT_ID, WIKI_PAGE_NAME); - await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetWikiPageAsync(PROJECT_ID, null, WIKI_PAGE_NAME)); - } - - } -} \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Tests/RedmineTest.cs b/xUnitTest-redmine-net45-api/Tests/RedmineTest.cs deleted file mode 100755 index 419af18e..00000000 --- a/xUnitTest-redmine-net45-api/Tests/RedmineTest.cs +++ /dev/null @@ -1,51 +0,0 @@ -ο»Ώusing System; -using System.Runtime.Remoting; -using Redmine.Net.Api; -using Redmine.Net.Api.Exceptions; -using Xunit; - -namespace xUnitTestredminenet45api.Tests -{ - [Trait("Redmine-api", "Credentials")] - [Collection("RedmineCollection")] - [Order(1)] - public class RedmineTest - { - - [Fact] - public void Should_Throw_Redmine_Exception_When_Host_Is_Null() - { - Assert.Throws(() => new RedmineManager(null, Helper.Username, Helper.Password)); - } - - [Fact] - public void Should_Throw_Redmine_Exception_When_Host_Is_Empty() - { - Assert.Throws(() => new RedmineManager(String.Empty, Helper.Username, Helper.Password)); - } - - [Fact] - public void Should_Throw_Redmine_Exception_When_Host_Is_Invalid() - { - Assert.Throws(() => new RedmineManager("invalid<>", Helper.Username, Helper.Password)); - } - - [Fact] - public void Should_Connect_With_Username_And_Password() - { - var a = new RedmineManager(Helper.Uri, Helper.Username, Helper.Password); - var currentUser = a.GetCurrentUser(); - Assert.NotNull(currentUser); - Assert.True(currentUser.Login.Equals(Helper.Username), "usernames not equals."); - } - - [Fact] - public void Should_Connect_With_Api_Key() - { - var a = new RedmineManager(Helper.Uri, Helper.ApiKey); - var currentUser = a.GetCurrentUser(); - Assert.NotNull(currentUser); - Assert.True(currentUser.ApiKey.Equals(Helper.ApiKey),"api keys not equals."); - } - } -} diff --git a/xUnitTest-redmine-net45-api/Tests/Sync/AttachmentTests.cs b/xUnitTest-redmine-net45-api/Tests/Sync/AttachmentTests.cs deleted file mode 100644 index 299a8fa0..00000000 --- a/xUnitTest-redmine-net45-api/Tests/Sync/AttachmentTests.cs +++ /dev/null @@ -1,121 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.IO; -using Redmine.Net.Api; -using Redmine.Net.Api.Types; -using Xunit; -using Redmine.Net.Api.Exceptions; - -namespace xUnitTestredminenet45api -{ - [Trait("Redmine-Net-Api", "Attachments")] - [Collection("RedmineCollection")] - public class AttachmentTests - { - public AttachmentTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - private const string ATTACHMENT_ID = "48"; - private const string ATTACHMENT_FILE_NAME = "uploadAttachment.pages"; - - [Fact, Order(1)] - public void Should_Download_Attachment() - { - var url = Helper.Uri + "/attachments/download/" + ATTACHMENT_ID + "/" + ATTACHMENT_FILE_NAME; - - var document = fixture.RedmineManager.DownloadFile(url); - - Assert.NotNull(document); - } - - [Fact, Order(2)] - public void Should_Get_Attachment_By_Id() - { - var attachment = fixture.RedmineManager.GetObject(ATTACHMENT_ID, null); - - Assert.NotNull(attachment); - Assert.IsType(attachment); - Assert.True(attachment.FileName == ATTACHMENT_FILE_NAME, "Attachment file name ( " + attachment.FileName + " ) " + - "is not the expected one ( " + ATTACHMENT_FILE_NAME + " )."); - } - - [Fact, Order(3)] - public void Should_Upload_Attachment() - { - const string ATTACHMENT_LOCAL_PATH = "uploadAttachment.pages"; - const string ATTACHMENT_NAME = "AttachmentUploaded.txt"; - const string ATTACHMENT_DESCRIPTION = "File uploaded using REST API"; - const string ATTACHMENT_CONTENT_TYPE = "text/plain"; - const int PROJECT_ID = 9; - const string ISSUE_SUBJECT = "Issue with attachments"; - - //read document from specified path - var documentData = System.IO.File.ReadAllBytes(AppDomain.CurrentDomain.BaseDirectory + ATTACHMENT_LOCAL_PATH); - - //upload attachment to redmine - var attachment = fixture.RedmineManager.UploadFile(documentData); - - //set attachment properties - attachment.FileName = ATTACHMENT_NAME; - attachment.Description = ATTACHMENT_DESCRIPTION; - attachment.ContentType = ATTACHMENT_CONTENT_TYPE; - - //create list of attachments to be added to issue - IList attachments = new List(); - attachments.Add(attachment); - - var issue = new Issue - { - Project = new Project { Id = PROJECT_ID }, - Subject = ISSUE_SUBJECT, - Uploads = attachments - }; - - //create issue and attach document - var issueWithAttachment = fixture.RedmineManager.CreateObject(issue); - - issue = fixture.RedmineManager.GetObject(issueWithAttachment.Id.ToString(), - new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.ATTACHMENTS } }); - - Assert.NotNull(issue); - Assert.NotNull(issue.Attachments); - Assert.All(issue.Attachments, a => Assert.IsType(a)); - Assert.True(issue.Attachments.Count == 1, "Number of attachments ( " + issue.Attachments.Count + " ) != 1"); - - var firstAttachment = issue.Attachments[0]; - Assert.True(firstAttachment.FileName == ATTACHMENT_NAME, "Attachment name is invalid."); - Assert.True(firstAttachment.Description == ATTACHMENT_DESCRIPTION,"Attachment description is invalid."); - Assert.True(firstAttachment.ContentType == ATTACHMENT_CONTENT_TYPE,"Attachment content type is invalid."); - } - - - [Fact, Order(4)] - public void Should_Delete_Attachment() - { - var exception = (RedmineException)Record.Exception(() => fixture.RedmineManager.DeleteObject(ATTACHMENT_ID)); - Assert.Null(exception); - Assert.Throws(() => fixture.RedmineManager.GetObject(ATTACHMENT_ID, null)); - } - } -} \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Tests/Sync/CustomFieldTests.cs b/xUnitTest-redmine-net45-api/Tests/Sync/CustomFieldTests.cs deleted file mode 100755 index 14715666..00000000 --- a/xUnitTest-redmine-net45-api/Tests/Sync/CustomFieldTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using Redmine.Net.Api.Types; -using Xunit; - -namespace xUnitTestredminenet45api -{ - [Trait("Redmine-Net-Api", "CustomFields")] - [Collection("RedmineCollection")] - public class CustomFieldTests - { - public CustomFieldTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - [Fact, Order(1)] - public void Should_Get_All_CustomFields() - { - const int NUMBER_OF_CUSTOM_FIELDS = 10; - - var customFields = fixture.RedmineManager.GetObjects(); - - Assert.NotNull(customFields); - Assert.All(customFields, cf => Assert.IsType(cf)); - Assert.True(customFields.Count == NUMBER_OF_CUSTOM_FIELDS, - "Custom fields count(" + customFields.Count + ") != " + NUMBER_OF_CUSTOM_FIELDS); - } - } -} \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Tests/Sync/GroupTests.cs b/xUnitTest-redmine-net45-api/Tests/Sync/GroupTests.cs deleted file mode 100755 index 55d71c05..00000000 --- a/xUnitTest-redmine-net45-api/Tests/Sync/GroupTests.cs +++ /dev/null @@ -1,133 +0,0 @@ -ο»Ώusing System.Collections.Generic; -using System.Collections.Specialized; -using Redmine.Net.Api; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Types; -using Xunit; - -namespace xUnitTestredminenet45api -{ - [Trait("Redmine-Net-Api", "Groups")] - [Collection("RedmineCollection")] - public class GroupTests - { - public GroupTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - private const string GROUP_ID = "57"; - private const int NUMBER_OF_MEMBERSHIPS = 1; - private const int NUMBER_OF_USERS = 2; - - [Fact, Order(1)] - public void Should_Add_Group() - { - const string NEW_GROUP_NAME = "Developers1"; - const int NEW_GROUP_USER_ID = 8; - - var group = new Group(); - group.Name = NEW_GROUP_NAME; - group.Users = new List {new GroupUser {Id = NEW_GROUP_USER_ID}}; - - Group savedGroup = null; - var exception = - (RedmineException) Record.Exception(() => savedGroup = fixture.RedmineManager.CreateObject(group)); - - Assert.Null(exception); - Assert.NotNull(savedGroup); - Assert.True(group.Name.Equals(savedGroup.Name), "Group name is not valid."); - } - - [Fact, Order(2)] - public void Should_Update_Group() - { - const string UPDATED_GROUP_ID = "58"; - const string UPDATED_GROUP_NAME = "Best Developers"; - const int UPDATED_GROUP_USER_ID = 2; - - var group = fixture.RedmineManager.GetObject(UPDATED_GROUP_ID, - new NameValueCollection {{RedmineKeys.INCLUDE, RedmineKeys.USERS}}); - group.Name = UPDATED_GROUP_NAME; - group.Users.Add(new GroupUser {Id = UPDATED_GROUP_USER_ID}); - - fixture.RedmineManager.UpdateObject(UPDATED_GROUP_ID, group); - - var updatedGroup = fixture.RedmineManager.GetObject(UPDATED_GROUP_ID, - new NameValueCollection {{RedmineKeys.INCLUDE, RedmineKeys.USERS}}); - - Assert.NotNull(updatedGroup); - Assert.True(updatedGroup.Name.Equals(UPDATED_GROUP_NAME), "Group name was not updated."); - Assert.NotNull(updatedGroup.Users); - Assert.All(updatedGroup.Users, u => Assert.IsType(u)); - Assert.True(updatedGroup.Users.Find(u => u.Id == UPDATED_GROUP_USER_ID) != null, - "User was not added to group."); - } - - [Fact, Order(3)] - public void Should_Get_All_Groups() - { - const int NUMBER_OF_GROUPS = 3; - - var groups = fixture.RedmineManager.GetObjects(); - - Assert.NotNull(groups); - Assert.All(groups, g => Assert.IsType(g)); - Assert.True(groups.Count == NUMBER_OF_GROUPS, "Number of groups ( "+groups.Count+" ) != " + NUMBER_OF_GROUPS); - } - - [Fact, Order(4)] - public void Should_Get_Group_With_All_Associated_Data() - { - var group = fixture.RedmineManager.GetObject(GROUP_ID, - new NameValueCollection {{RedmineKeys.INCLUDE, RedmineKeys.MEMBERSHIPS + "," + RedmineKeys.USERS}}); - - Assert.NotNull(group); - - Assert.All(group.Memberships, m => Assert.IsType(m)); - Assert.True(group.Memberships.Count == NUMBER_OF_MEMBERSHIPS, - "Number of memberships != " + NUMBER_OF_MEMBERSHIPS); - - Assert.All(group.Users, u => Assert.IsType(u)); - Assert.True(group.Users.Count == NUMBER_OF_USERS, "Number of users ( "+ group.Users.Count +" ) != " + NUMBER_OF_USERS); - Assert.True(group.Name.Equals("Test"), "Group name is not valid."); - } - - [Fact, Order(5)] - public void Should_Get_Group_With_Memberships() - { - var group = fixture.RedmineManager.GetObject(GROUP_ID, - new NameValueCollection {{RedmineKeys.INCLUDE, RedmineKeys.MEMBERSHIPS}}); - - Assert.NotNull(group); - Assert.All(group.Memberships, m => Assert.IsType(m)); - Assert.True(group.Memberships.Count == NUMBER_OF_MEMBERSHIPS, - "Number of memberships ( "+ group.Memberships.Count +" ) != " + NUMBER_OF_MEMBERSHIPS); - } - - [Fact, Order(6)] - public void Should_Get_Group_With_Users() - { - var group = fixture.RedmineManager.GetObject(GROUP_ID, - new NameValueCollection {{RedmineKeys.INCLUDE, RedmineKeys.USERS}}); - - Assert.NotNull(group); - Assert.All(group.Users, u => Assert.IsType(u)); - Assert.True(group.Users.Count == NUMBER_OF_USERS, "Number of users ( "+ group.Users.Count +" ) != " + NUMBER_OF_USERS); - } - - [Fact, Order(99)] - public void Should_Delete_Group() - { - const string DELETED_GROUP_ID = "63"; - - var exception = - (RedmineException) - Record.Exception(() => fixture.RedmineManager.DeleteObject(DELETED_GROUP_ID)); - Assert.Null(exception); - Assert.Throws(() => fixture.RedmineManager.GetObject(DELETED_GROUP_ID, null)); - } - } -} \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Tests/Sync/IssueCategoryTests.cs b/xUnitTest-redmine-net45-api/Tests/Sync/IssueCategoryTests.cs deleted file mode 100755 index cc626486..00000000 --- a/xUnitTest-redmine-net45-api/Tests/Sync/IssueCategoryTests.cs +++ /dev/null @@ -1,132 +0,0 @@ -ο»Ώusing System; -using System.Collections.Specialized; -using Redmine.Net.Api; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Types; -using Xunit; - -namespace xUnitTestredminenet45api -{ - [Trait("Redmine-Net-Api", "IssueCategories")] - [Collection("RedmineCollection")] - public class IssueCategoryTests - { - public IssueCategoryTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - const string PROJECT_ID = "redmine-net-testq"; - const string NEW_ISSUE_CATEGORY_NAME = "Test category"; - const int NEW_ISSUE_CATEGORY_ASIGNEE_ID = 1; - - private static string CREATED_ISSUE_CATEGORY_ID; - - [Fact, Order(1)] - public void Should_Create_IssueCategory() - { - var issueCategory = new IssueCategory - { - Name = NEW_ISSUE_CATEGORY_NAME, - AsignTo = new IdentifiableName {Id = NEW_ISSUE_CATEGORY_ASIGNEE_ID} - }; - - var savedIssueCategory = fixture.RedmineManager.CreateObject(issueCategory, PROJECT_ID); - - CREATED_ISSUE_CATEGORY_ID = savedIssueCategory.Id.ToString(); - - Assert.NotNull(savedIssueCategory); - Assert.True(savedIssueCategory.Name.Equals(NEW_ISSUE_CATEGORY_NAME), "Saved issue category name is invalid."); - } - - [Fact, Order(99)] - public void Should_Delete_IssueCategory() - { - var exception = - (RedmineException) - Record.Exception( - () => fixture.RedmineManager.DeleteObject(CREATED_ISSUE_CATEGORY_ID)); - Assert.Null(exception); - Assert.Throws( - () => fixture.RedmineManager.GetObject(CREATED_ISSUE_CATEGORY_ID, null)); - } - - [Fact, Order(2)] - public void Should_Get_All_IssueCategories_By_ProjectId() - { - const int NUMBER_OF_ISSUE_CATEGORIES = 3; - var issueCategories = - fixture.RedmineManager.GetObjects(new NameValueCollection - { - {RedmineKeys.PROJECT_ID, PROJECT_ID} - }); - - Assert.NotNull(issueCategories); - Assert.All(issueCategories, ic => Assert.IsType(ic)); - Assert.True(issueCategories.Count == NUMBER_OF_ISSUE_CATEGORIES, - "Number of issue categories ( "+issueCategories.Count+" ) != " + NUMBER_OF_ISSUE_CATEGORIES); - } - - [Fact, Order(3)] - public void Should_Get_IssueCategory_By_Id() - { - const string ISSUE_CATEGORY_PROJECT_NAME_TO_GET = "Redmine tests"; - const string ISSUE_CATEGORY_ASIGNEE_NAME_TO_GET = "Redmine"; - - var issueCategory = fixture.RedmineManager.GetObject(CREATED_ISSUE_CATEGORY_ID, null); - - Assert.NotNull(issueCategory); - Assert.True(issueCategory.Name.Equals(NEW_ISSUE_CATEGORY_NAME), "Issue category name is invalid."); - Assert.NotNull(issueCategory.AsignTo); - Assert.True(issueCategory.AsignTo.Name.Contains(ISSUE_CATEGORY_ASIGNEE_NAME_TO_GET), - "Asignee name is invalid."); - Assert.NotNull(issueCategory.Project); - Assert.True(issueCategory.Project.Name.Equals(ISSUE_CATEGORY_PROJECT_NAME_TO_GET), - "Project name is invalid."); - } - - [Fact, Order(4)] - public void Should_Update_IssueCategory() - { - const string ISSUE_CATEGORY_NAME_TO_UPDATE = "Category updated"; - const int ISSUE_CATEGORY_ASIGNEE_ID_TO_UPDATE = 2; - - var issueCategory = fixture.RedmineManager.GetObject(CREATED_ISSUE_CATEGORY_ID, null); - issueCategory.Name = ISSUE_CATEGORY_NAME_TO_UPDATE; - issueCategory.AsignTo = new IdentifiableName {Id = ISSUE_CATEGORY_ASIGNEE_ID_TO_UPDATE}; - - fixture.RedmineManager.UpdateObject(CREATED_ISSUE_CATEGORY_ID, issueCategory); - - var updatedIssueCategory = fixture.RedmineManager.GetObject(CREATED_ISSUE_CATEGORY_ID, null); - - Assert.NotNull(updatedIssueCategory); - Assert.True(updatedIssueCategory.Name.Equals(ISSUE_CATEGORY_NAME_TO_UPDATE), - "Issue category name was not updated."); - Assert.NotNull(updatedIssueCategory.AsignTo); - Assert.True(updatedIssueCategory.AsignTo.Id == ISSUE_CATEGORY_ASIGNEE_ID_TO_UPDATE, - "Issue category asignee was not updated."); - } - - [Fact, Order(5)] - public void Should_Reassign_Issue_After_Issue_Category_Is_Deleted() - { - const string ISSUE_CATEGORY_ID_TO_DELETE = "8"; - const string ISSUE_CATEGORY_ID_TO_REASSIGN = "10"; - const string ISSUE_ID_REASSIGNED = "9"; - - var exception = - (RedmineException) - Record.Exception( - () => fixture.RedmineManager.DeleteObject(ISSUE_CATEGORY_ID_TO_DELETE, new NameValueCollection{{RedmineKeys.REASSIGN_TO_ID, ISSUE_CATEGORY_ID_TO_REASSIGN}})); - Assert.Null(exception); - Assert.Throws( - () => fixture.RedmineManager.GetObject(ISSUE_CATEGORY_ID_TO_DELETE, null)); - - var issue = fixture.RedmineManager.GetObject(ISSUE_ID_REASSIGNED, null); - Assert.NotNull(issue.Category); - Assert.True(ISSUE_CATEGORY_ID_TO_REASSIGN.Equals(issue.Category.Id.ToString()), string.Format("Issue was not reassigned. Issue category id {0} != {1}", issue.Category.Id, ISSUE_CATEGORY_ID_TO_REASSIGN)); - } - } -} \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Tests/Sync/IssuePriorityTests.cs b/xUnitTest-redmine-net45-api/Tests/Sync/IssuePriorityTests.cs deleted file mode 100755 index dbe51317..00000000 --- a/xUnitTest-redmine-net45-api/Tests/Sync/IssuePriorityTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using Redmine.Net.Api.Types; -using Xunit; - -namespace xUnitTestredminenet45api -{ - [Trait("Redmine-Net-Api", "IssuePriorities")] - [Collection("RedmineCollection")] - public class IssuePriorityTests - { - public IssuePriorityTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - [Fact] - public void Should_Get_All_Issue_Priority() - { - const int NUMBER_OF_ISSUE_PRIORITIES = 4; - var issuePriorities = fixture.RedmineManager.GetObjects(); - - Assert.NotNull(issuePriorities); - Assert.All(issuePriorities, ip => Assert.IsType(ip)); - Assert.True(issuePriorities.Count == NUMBER_OF_ISSUE_PRIORITIES, - "Issue priorities count(" + issuePriorities.Count + ") != " + NUMBER_OF_ISSUE_PRIORITIES); - } - } -} \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Tests/Sync/IssueRelationTests.cs b/xUnitTest-redmine-net45-api/Tests/Sync/IssueRelationTests.cs deleted file mode 100755 index 06d677a0..00000000 --- a/xUnitTest-redmine-net45-api/Tests/Sync/IssueRelationTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System.Collections.Specialized; -using Redmine.Net.Api; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Types; -using Xunit; - -namespace xUnitTestredminenet45api -{ - [Trait("Redmine-Net-Api", "IssueRelations")] - [Collection("RedmineCollection")] - public class IssueRelationTests - { - public IssueRelationTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - private const string ISSUE_ID = "96"; - private const int RELATED_ISSUE_ID = 94; - private const int RELATION_DELAY = 2; - - private const IssueRelationType OPPOSED_RELATION_TYPE = IssueRelationType.precedes; - - [Fact, Order(1)] - public void Should_Add_Issue_Relation() - { - const IssueRelationType RELATION_TYPE = IssueRelationType.follows; - var relation = new IssueRelation - { - IssueToId = RELATED_ISSUE_ID, - Type = RELATION_TYPE, - Delay = RELATION_DELAY - }; - - var savedRelation = fixture.RedmineManager.CreateObject(relation, ISSUE_ID); - - Assert.NotNull(savedRelation); - Assert.True(savedRelation.IssueId == RELATED_ISSUE_ID, "Related issue id is not valid."); - Assert.True(savedRelation.IssueToId.ToString().Equals(ISSUE_ID), "Issue id is not valid."); - Assert.True(savedRelation.Delay == RELATION_DELAY, "Delay is not valid."); - Assert.True(savedRelation.Type == OPPOSED_RELATION_TYPE, "Relation type is not valid."); - } - - [Fact, Order(4)] - public void Should_Delete_Issue_Relation() - { - const string RELATION_ID_TO_DELETE = "23"; - var exception = - (RedmineException) - Record.Exception( - () => fixture.RedmineManager.DeleteObject(RELATION_ID_TO_DELETE)); - Assert.Null(exception); - Assert.Throws( - () => fixture.RedmineManager.GetObject(RELATION_ID_TO_DELETE, null)); - } - - [Fact, Order(2)] - public void Should_Get_IssueRelation_By_Id() - { - const string RELATION_ID_TO_GET = "27"; - var relation = fixture.RedmineManager.GetObject(RELATION_ID_TO_GET, null); - - Assert.NotNull(relation); - Assert.True(relation.IssueId == RELATED_ISSUE_ID, "Related issue id is not valid."); - Assert.True(relation.IssueToId.ToString().Equals(ISSUE_ID), "Issue id is not valid."); - Assert.True(relation.Delay == RELATION_DELAY, "Delay is not valid."); - Assert.True(relation.Type == OPPOSED_RELATION_TYPE, "Relation type is not valid."); - } - - [Fact, Order(3)] - public void Should_Get_IssueRelations_By_Issue_Id() - { - const int NUMBER_OF_RELATIONS = 1; - var relations = - fixture.RedmineManager.GetObjects(new NameValueCollection - { - {RedmineKeys.ISSUE_ID, ISSUE_ID} - }); - - Assert.NotNull(relations); - Assert.True(relations.Count == NUMBER_OF_RELATIONS, "Number of issue relations ( "+relations.Count+" ) != " + NUMBER_OF_RELATIONS); - } - } -} \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Tests/Sync/IssueStatusTests.cs b/xUnitTest-redmine-net45-api/Tests/Sync/IssueStatusTests.cs deleted file mode 100755 index bd9f978c..00000000 --- a/xUnitTest-redmine-net45-api/Tests/Sync/IssueStatusTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using Redmine.Net.Api.Types; -using Xunit; - -namespace xUnitTestredminenet45api -{ - [Trait("Redmine-Net-Api", "IssueStatuses")] - [Collection("RedmineCollection")] - public class IssueStatusTests - { - public IssueStatusTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - [Fact] - public void Should_Get_All_Issue_Statuses() - { - const int NUMBER_OF_ISSUE_STATUSES = 7; - var issueStatuses = fixture.RedmineManager.GetObjects(); - - Assert.NotNull(issueStatuses); - Assert.All(issueStatuses, i => Assert.IsType(i)); - Assert.True(issueStatuses.Count == NUMBER_OF_ISSUE_STATUSES, - "Issue statuses count(" + issueStatuses.Count + ") != " + NUMBER_OF_ISSUE_STATUSES); - } - } -} \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Tests/Sync/IssueTests.cs b/xUnitTest-redmine-net45-api/Tests/Sync/IssueTests.cs deleted file mode 100755 index 4648b884..00000000 --- a/xUnitTest-redmine-net45-api/Tests/Sync/IssueTests.cs +++ /dev/null @@ -1,308 +0,0 @@ -ο»Ώusing System; -using System.Collections.Specialized; -using System.Collections.Generic; -using Xunit; -using Redmine.Net.Api; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Exceptions; - -namespace xUnitTestredminenet45api -{ - [Trait("Redmine-Net-Api", "Issues")] - [Collection("RedmineCollection")] - public class IssueTests - { - public IssueTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - private readonly RedmineFixture fixture; - - //filters - private const string PROJECT_ID = "redmine-net-testq"; - - //watcher - private const int WATCHER_ISSUE_ID = 96; - private const int WATCHER_USER_ID = 8; - - - [Fact, Order(1)] - public void Should_Get_All_Issues() - { - var issues = fixture.RedmineManager.GetObjects(); - - Assert.NotNull(issues); - Assert.All (issues, i => Assert.IsType (i)); - } - - [Fact, Order(2)] - public void Should_Get_Paginated_Issues() - { - const int NUMBER_OF_PAGINATED_ISSUES = 3; - const int OFFSET = 1; - - var issues = fixture.RedmineManager.GetPaginatedObjects(new NameValueCollection { { RedmineKeys.OFFSET, OFFSET.ToString() }, { RedmineKeys.LIMIT, NUMBER_OF_PAGINATED_ISSUES.ToString() }, { "sort", "id:desc" } }); - - Assert.NotNull(issues.Objects); - Assert.All (issues.Objects, i => Assert.IsType (i)); - Assert.True(issues.Objects.Count <= NUMBER_OF_PAGINATED_ISSUES, "number of issues ( "+ issues.Objects.Count +" ) != " + NUMBER_OF_PAGINATED_ISSUES.ToString()); - } - - [Fact, Order(3)] - public void Should_Get_Issues_By_Project_Id() - { - var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.PROJECT_ID, PROJECT_ID } }); - - Assert.NotNull(issues); - Assert.All (issues, i => Assert.IsType (i)); - } - - [Fact, Order(4)] - public void Should_Get_Issues_By_subproject_Id() - { - const string SUBPROJECT_ID = "redmine-net-testr"; - - var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.SUBPROJECT_ID, SUBPROJECT_ID } }); - - Assert.NotNull(issues); - Assert.All (issues, i => Assert.IsType (i)); - } - - [Fact, Order(5)] - public void Should_Get_Issues_By_Project_Without_Subproject() - { - const string ALL_SUBPROJECTS = "!*"; - - var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.PROJECT_ID, PROJECT_ID }, { RedmineKeys.SUBPROJECT_ID, ALL_SUBPROJECTS } }); - - Assert.NotNull(issues); - Assert.All (issues, i => Assert.IsType (i)); - } - - [Fact, Order(6)] - public void Should_Get_Issues_By_Tracker() - { - const string TRACKER_ID = "3"; - var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.TRACKER_ID, TRACKER_ID } }); - - Assert.NotNull(issues); - Assert.All (issues, i => Assert.IsType (i)); - } - - [Fact, Order(7)] - public void Should_Get_Issues_By_Status() - { - const string STATUS_ID = "*"; - var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.STATUS_ID, STATUS_ID } }); - Assert.NotNull(issues); - Assert.All (issues, i => Assert.IsType (i)); - } - - [Fact, Order(8)] - public void Should_Get_Issues_By_Asignee() - { - const string ASSIGNED_TO_ID = "me"; - var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.ASSIGNED_TO_ID, ASSIGNED_TO_ID } }); - - Assert.NotNull(issues); - Assert.All (issues, i => Assert.IsType (i)); - } - - [Fact, Order(9)] - public void Should_Get_Issues_By_Custom_Field() - { - const string CUSTOM_FIELD_NAME = "cf_13"; - const string CUSTOM_FIELD_VALUE = "Testx"; - - var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { CUSTOM_FIELD_NAME, CUSTOM_FIELD_VALUE } }); - - Assert.NotNull(issues); - Assert.All (issues, i => Assert.IsType (i)); - } - - [Fact, Order(10)] - public void Should_Get_Issue_By_Id() - { - const string ISSUE_ID = "96"; - - var issue = fixture.RedmineManager.GetObject(ISSUE_ID, new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.CHILDREN + "," + RedmineKeys.ATTACHMENTS + "," + RedmineKeys.RELATIONS + "," + RedmineKeys.CHANGESETS + "," + RedmineKeys.JOURNALS + "," + RedmineKeys.WATCHERS } }); - - Assert.NotNull(issue); - //TODO: add conditions for all associated data if nedeed - } - - [Fact, Order(11)] - public void Should_Add_Issue() - { - const int NEW_ISSUE_PROJECT_ID = 9; - const int NEW_ISSUE_TRACKER_ID = 3; - const int NEW_ISSUE_STATUS_ID = 6; - const int NEW_ISSUE_PRIORITY_ID = 9; - const string NEW_ISSUE_SUBJECT = "Issue created using Rest API"; - const string NEW_ISSUE_DESCRIPTION = "Issue description..."; - const int NEW_ISSUE_CATEGORY_ID = 18; - const int NEW_ISSUE_FIXED_VERSION_ID = 9; - const int NEW_ISSUE_ASSIGNED_TO_ID = 8; - const int NEW_ISSUE_PARENT_ISSUE_ID = 96; - const int NEW_ISSUE_CUSTOM_FIELD_ID = 13; - const string NEW_ISSUE_CUSTOM_FIELD_VALUE = "Issue custom field completed"; - const bool NEW_ISSUE_IS_PRIVATE = true; - const int NEW_ISSUE_ESTIMATED_HOURS = 12; - DateTime NEW_ISSUE_START_DATE = DateTime.Now; - DateTime NEW_ISSUE_DUE_DATE = DateTime.Now.AddDays(10); - const int NEW_ISSUE_FIRST_WATCHER_ID = 2; - const int NEW_ISSUE_SECOND_WATCHER_ID = 8; - - Issue issue = new Issue - { - Project = new Project {Id = NEW_ISSUE_PROJECT_ID}, - Tracker = new IdentifiableName {Id = NEW_ISSUE_TRACKER_ID}, - Status = new IdentifiableName {Id = NEW_ISSUE_STATUS_ID}, - Priority = new IdentifiableName {Id = NEW_ISSUE_PRIORITY_ID}, - Subject = NEW_ISSUE_SUBJECT, - Description = NEW_ISSUE_DESCRIPTION, - Category = new IdentifiableName {Id = NEW_ISSUE_CATEGORY_ID}, - FixedVersion = new IdentifiableName {Id = NEW_ISSUE_FIXED_VERSION_ID}, - AssignedTo = new IdentifiableName {Id = NEW_ISSUE_ASSIGNED_TO_ID}, - ParentIssue = new IdentifiableName {Id = NEW_ISSUE_PARENT_ISSUE_ID}, - CustomFields = new List - { - new IssueCustomField - { - Id = NEW_ISSUE_CUSTOM_FIELD_ID, - Values = new List {new CustomFieldValue {Info = NEW_ISSUE_CUSTOM_FIELD_VALUE}} - } - }, - IsPrivate = NEW_ISSUE_IS_PRIVATE, - EstimatedHours = NEW_ISSUE_ESTIMATED_HOURS, - StartDate = NEW_ISSUE_START_DATE, - DueDate = NEW_ISSUE_DUE_DATE, - Watchers = new List - { - new Watcher {Id = NEW_ISSUE_FIRST_WATCHER_ID}, - new Watcher {Id = NEW_ISSUE_SECOND_WATCHER_ID} - } - }; - - Issue savedIssue = fixture.RedmineManager.CreateObject(issue); - - Assert.NotNull(savedIssue); - Assert.True(issue.Subject.Equals(savedIssue.Subject), "Issue subject is invalid."); - Assert.NotEqual(issue, savedIssue); - } - - [Fact, Order(12)] - public void Should_Update_Issue() - { - const string UPDATED_ISSUE_ID = "98"; - const string UPDATED_ISSUE_SUBJECT = "Issue updated subject"; - const string UPDATED_ISSUE_DESCRIPTION = null; - DateTime? UPDATED_ISSUE_START_DATE = null; - DateTime UPDATED_ISSUE_DUE_DATE = DateTime.Now.AddMonths(1); - const int UPDATED_ISSUE_PROJECT_ID = 9; - const int UPDATED_ISSUE_TRACKER_ID = 3; - const int UPDATED_ISSUE_PRIORITY_ID = 8; - const int UPDATED_ISSUE_CATEGORY_ID = 18; - const int UPDATED_ISSUE_ASSIGNED_TO_ID = 2; - const int UPDATED_ISSUE_PARENT_ISSUE_ID = 91; - const int UPDATED_ISSUE_CUSTOM_FIELD_ID = 13; - const string UPDATED_ISSUE_CUSTOM_FIELD_VALUE = "Another custom field completed"; - const int UPDATED_ISSUE_ESTIMATED_HOURS = 23; - const string UPDATED_ISSUE_NOTES = "A lot is changed"; - const bool UPDATED_ISSUE_PRIVATE_NOTES = true; - - var issue = fixture.RedmineManager.GetObject(UPDATED_ISSUE_ID, new NameValueCollection { { "include", "children,attachments,relations,changesets,journals,watchers" } }); - issue.Subject = UPDATED_ISSUE_SUBJECT; - issue.Description = UPDATED_ISSUE_DESCRIPTION; - issue.StartDate = UPDATED_ISSUE_START_DATE; - issue.DueDate = UPDATED_ISSUE_DUE_DATE; - issue.Project.Id = UPDATED_ISSUE_PROJECT_ID; - issue.Tracker.Id = UPDATED_ISSUE_TRACKER_ID; - issue.Priority.Id = UPDATED_ISSUE_PRIORITY_ID; - issue.Category.Id = UPDATED_ISSUE_CATEGORY_ID; - issue.AssignedTo.Id = UPDATED_ISSUE_ASSIGNED_TO_ID; - issue.ParentIssue.Id = UPDATED_ISSUE_PARENT_ISSUE_ID; - - if (issue.CustomFields != null) - issue.CustomFields.Add(new IssueCustomField { Id = UPDATED_ISSUE_CUSTOM_FIELD_ID, Values = new List { new CustomFieldValue { Info = UPDATED_ISSUE_CUSTOM_FIELD_VALUE } } }); - issue.EstimatedHours = UPDATED_ISSUE_ESTIMATED_HOURS; - issue.Notes = UPDATED_ISSUE_NOTES; - issue.PrivateNotes = UPDATED_ISSUE_PRIVATE_NOTES; - - fixture.RedmineManager.UpdateObject(UPDATED_ISSUE_ID, issue); - - var updatedIssue = fixture.RedmineManager.GetObject(UPDATED_ISSUE_ID, new NameValueCollection { { "include", "children,attachments,relations,changesets,journals,watchers" } }); - - Assert.NotNull(updatedIssue); - Assert.True(issue.Subject.Equals(updatedIssue.Subject), "Issue subject is invalid."); - - } - - [Fact, Order(99)] - public void Should_Delete_Issue() - { - const string DELETED_ISSUE_ID = "90"; - - RedmineException exception = (RedmineException)Record.Exception(() => fixture.RedmineManager.DeleteObject(DELETED_ISSUE_ID)); - Assert.Null (exception); - Assert.Throws(() => fixture.RedmineManager.GetObject(DELETED_ISSUE_ID, null)); - } - - [Fact, Order(13)] - public void Should_Add_Watcher_To_Issue() - { - fixture.RedmineManager.AddWatcherToIssue(WATCHER_ISSUE_ID, WATCHER_USER_ID); - - Issue issue = fixture.RedmineManager.GetObject(WATCHER_ISSUE_ID.ToString(), new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.WATCHERS } }); - - Assert.NotNull(issue); - Assert.NotNull(issue.Watchers); - Assert.True(((List)issue.Watchers).Find(w => w.Id == WATCHER_USER_ID) != null, "Watcher was not added."); - } - - [Fact, Order(14)] - public void Should_Remove_Watcher_From_Issue() - { - fixture.RedmineManager.RemoveWatcherFromIssue(WATCHER_ISSUE_ID, WATCHER_USER_ID); - - Issue issue = fixture.RedmineManager.GetObject(WATCHER_ISSUE_ID.ToString(), new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.WATCHERS } }); - - Assert.NotNull(issue); - Assert.True(issue.Watchers == null || ((List)issue.Watchers).Find(w => w.Id == WATCHER_USER_ID) == null, "Watcher was not removed."); - } - - [Fact, Order(15)] - public void Should_Clone_Issue() - { - const string ISSUE_TO_CLONE_SUBJECT = "Issue to clone"; - const int ISSUE_TO_CLONE_CUSTOM_FIELD_ID = 13; - const string ISSUE_TO_CLONE_CUSTOM_FIELD_VALUE = "Issue to clone custom field value"; - const int CLONED_ISSUE_CUSTOM_FIELD_ID = 13; - const string CLONED_ISSUE_CUSTOM_FIELD_VALUE = "Cloned issue custom field value"; - - var issueToClone = new Issue - { - Subject = ISSUE_TO_CLONE_SUBJECT, - CustomFields = new List - { - new IssueCustomField - { - Id = ISSUE_TO_CLONE_CUSTOM_FIELD_ID, - Values = - new List {new CustomFieldValue {Info = ISSUE_TO_CLONE_CUSTOM_FIELD_VALUE}} - } - } - }; - - - var clonedIssue = (Issue)issueToClone.Clone(); - clonedIssue.CustomFields.Add(new IssueCustomField - { - Id = CLONED_ISSUE_CUSTOM_FIELD_ID, - Values = new List { new CustomFieldValue { Info = CLONED_ISSUE_CUSTOM_FIELD_VALUE } } - }); - - Assert.True(issueToClone.CustomFields.Count != clonedIssue.CustomFields.Count); - } - } -} \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Tests/Sync/NewsTests.cs b/xUnitTest-redmine-net45-api/Tests/Sync/NewsTests.cs deleted file mode 100755 index 3cf4fae7..00000000 --- a/xUnitTest-redmine-net45-api/Tests/Sync/NewsTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System.Collections.Specialized; -using Redmine.Net.Api; -using Redmine.Net.Api.Types; -using Xunit; - -namespace xUnitTestredminenet45api -{ - [Trait("Redmine-Net-Api", "News")] - [Collection("RedmineCollection")] - public class NewsTests - { - public NewsTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - [Fact, Order(1)] - public void Should_Get_All_News() - { - const int NUMBER_OF_NEWS = 2; - var news = fixture.RedmineManager.GetObjects(); - - Assert.NotNull(news); - Assert.All(news, n => Assert.IsType(n)); - Assert.True(news.Count == NUMBER_OF_NEWS, "News count(" + news.Count + ") != " + NUMBER_OF_NEWS); - } - - [Fact, Order(2)] - public void Should_Get_News_By_Project_Id() - { - const string PROJECT_ID = "redmine-net-testq"; - const int NUMBER_OF_NEWS_BY_PROJECT_ID = 1; - var news = - fixture.RedmineManager.GetObjects(new NameValueCollection {{RedmineKeys.PROJECT_ID, PROJECT_ID}}); - - Assert.NotNull(news); - Assert.All(news, n => Assert.IsType(n)); - Assert.True(news.Count == NUMBER_OF_NEWS_BY_PROJECT_ID, - "News count(" + news.Count + ") != " + NUMBER_OF_NEWS_BY_PROJECT_ID); - } - } -} \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Tests/Sync/ProjectMembershipTests.cs b/xUnitTest-redmine-net45-api/Tests/Sync/ProjectMembershipTests.cs deleted file mode 100755 index 72a5c550..00000000 --- a/xUnitTest-redmine-net45-api/Tests/Sync/ProjectMembershipTests.cs +++ /dev/null @@ -1,125 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System.Collections.Generic; -using System.Collections.Specialized; -using Redmine.Net.Api; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Types; -using Xunit; - -namespace xUnitTestredminenet45api -{ - [Trait("Redmine-Net-Api", "ProjectMemberships")] - [Collection("RedmineCollection")] - public class ProjectMembershipTests - { - public ProjectMembershipTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - private const string PROJECT_IDENTIFIER = "redmine-net-testq"; - - - [Fact, Order(1)] - public void Should_Add_Project_Membership() - { - const int NEW_PROJECT_MEMBERSHIP_USER_ID = 2; - const int NEW_PROJECT_MEMBERSHIP_ROLE_ID = 5; - - var pm = new ProjectMembership - { - User = new IdentifiableName {Id = NEW_PROJECT_MEMBERSHIP_USER_ID}, - Roles = new List {new MembershipRole {Id = NEW_PROJECT_MEMBERSHIP_ROLE_ID}} - }; - - var createdPm = fixture.RedmineManager.CreateObject(pm, PROJECT_IDENTIFIER); - - Assert.NotNull(createdPm); - Assert.True(createdPm.User.Id == NEW_PROJECT_MEMBERSHIP_USER_ID, "User is invalid."); - Assert.NotNull(createdPm.Roles); - Assert.True(createdPm.Roles.Exists(r => r.Id == NEW_PROJECT_MEMBERSHIP_ROLE_ID), - string.Format("Role id {0} does not exist.", NEW_PROJECT_MEMBERSHIP_ROLE_ID)); - } - - [Fact,Order(99)] - public void Should_Delete_Project_Membership() - { - const string DELETED_PROJECT_MEMBERSHIP_ID = "142"; - var exception = - (RedmineException) - Record.Exception( - () => - fixture.RedmineManager.DeleteObject(DELETED_PROJECT_MEMBERSHIP_ID)); - Assert.Null(exception); - Assert.Throws( - () => fixture.RedmineManager.GetObject(DELETED_PROJECT_MEMBERSHIP_ID, null)); - } - - [Fact, Order(2)] - public void Should_Get_Memberships_By_Project_Identifier() - { - const int NUMBER_OF_PROJECT_MEMBERSHIPS = 3; - var projectMemberships = - fixture.RedmineManager.GetObjects(new NameValueCollection - { - {RedmineKeys.PROJECT_ID, PROJECT_IDENTIFIER} - }); - - Assert.NotNull(projectMemberships); - Assert.True(projectMemberships.Count == NUMBER_OF_PROJECT_MEMBERSHIPS, - "Project memberships count ( "+ projectMemberships.Count +" ) != " + NUMBER_OF_PROJECT_MEMBERSHIPS); - Assert.All(projectMemberships, pm => Assert.IsType(pm)); - } - - [Fact, Order(3)] - public void Should_Get_Project_Membership_By_Id() - { - const string PROJECT_MEMBERSHIP_ID = "143"; - var projectMembership = fixture.RedmineManager.GetObject(PROJECT_MEMBERSHIP_ID, null); - - Assert.NotNull(projectMembership); - Assert.NotNull(projectMembership.Project); - Assert.True(projectMembership.User != null || projectMembership.Group != null, - "User and group are both null."); - Assert.NotNull(projectMembership.Roles); - Assert.All(projectMembership.Roles, r => Assert.IsType(r)); - } - - [Fact, Order(4)] - public void Should_Update_Project_Membership() - { - const string UPDATED_PROJECT_MEMBERSHIP_ID = "143"; - const int UPDATED_PROJECT_MEMBERSHIP_ROLE_ID = 4; - - var pm = fixture.RedmineManager.GetObject(UPDATED_PROJECT_MEMBERSHIP_ID, null); - pm.Roles.Add(new MembershipRole {Id = UPDATED_PROJECT_MEMBERSHIP_ROLE_ID}); - - fixture.RedmineManager.UpdateObject(UPDATED_PROJECT_MEMBERSHIP_ID, pm); - - var updatedPm = fixture.RedmineManager.GetObject(UPDATED_PROJECT_MEMBERSHIP_ID, null); - - Assert.NotNull(updatedPm); - Assert.NotNull(updatedPm.Roles); - Assert.All(updatedPm.Roles, r => Assert.IsType(r)); - Assert.True(updatedPm.Roles.Find(r => r.Id == UPDATED_PROJECT_MEMBERSHIP_ROLE_ID) != null, - string.Format("Role with id {0} was not found in roles list.", UPDATED_PROJECT_MEMBERSHIP_ROLE_ID)); - } - } -} \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Tests/Sync/ProjectTests.cs b/xUnitTest-redmine-net45-api/Tests/Sync/ProjectTests.cs deleted file mode 100755 index fc7cbd31..00000000 --- a/xUnitTest-redmine-net45-api/Tests/Sync/ProjectTests.cs +++ /dev/null @@ -1,263 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System.Collections.Generic; -using System.Collections.Specialized; -using Redmine.Net.Api; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Types; -using Xunit; - -namespace xUnitTestredminenet45api -{ - [Trait("Redmine-Net-Api", "Projects")] - [Collection("RedmineCollection")] - [Order(1)] - public class ProjectTests - { - public ProjectTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private const string PROJECT_IDENTIFIER = "redmine-net-api-project-test"; - private const string PROJECT_NAME = "Redmine Net Api Project Test"; - - private readonly RedmineFixture fixture; - - private static Project CreateTestProjectWithRequiredPropertiesSet() - { - var project = new Project - { - Name = PROJECT_NAME, - Identifier = PROJECT_IDENTIFIER - }; - - return project; - } - - private static Project CreateTestProjectWithAllPropertiesSet() - { - var project = new Project - { - Name = "Redmine Net Api Project Test All Properties", - Description = "This is a test project.", - Identifier = "rnaptap", - HomePage = "www.redminetest.com", - IsPublic = true, - InheritMembers = true, - Status = ProjectStatus.Active, - EnabledModules = new List - { - new ProjectEnabledModule {Name = "issue_tracking"}, - new ProjectEnabledModule {Name = "time_tracking"} - }, - Trackers = new List - { - new ProjectTracker {Id = 1}, - new ProjectTracker {Id = 2} - } - }; - - return project; - } - - private static Project CreateTestProjectWithInvalidTrackersId() - { - var project = new Project - { - Name = "Redmine Net Api Project Test Invalid Trackers", - Identifier = "rnaptit", - Trackers = new List - { - new ProjectTracker {Id = 999999}, - new ProjectTracker {Id = 999998} - } - }; - - return project; - } - - private static Project CreateTestProjectWithParentSet(int parentId) - { - var project = new Project - { - Name = "Redmine Net Api Project With Parent Set", - Identifier = "rnapwps", - Parent = new IdentifiableName {Id = parentId} - }; - - return project; - } - - [Fact, Order(0)] - public void Should_Create_Project_With_Required_Properties() - { - var savedProject = fixture.RedmineManager.CreateObject(CreateTestProjectWithRequiredPropertiesSet()); - - Assert.NotNull(savedProject); - Assert.NotEqual(savedProject.Id, 0); - Assert.True(savedProject.Name.Equals(PROJECT_NAME), "Project name is invalid."); - Assert.True(savedProject.Identifier.Equals(PROJECT_IDENTIFIER), "Project identifier is invalid."); - } - - [Fact, Order(1)] - public void Should_Create_Project_With_All_Properties_Set() - { - var savedProject = fixture.RedmineManager.CreateObject(CreateTestProjectWithAllPropertiesSet()); - - Assert.NotNull(savedProject); - Assert.NotEqual(savedProject.Id, 0); - Assert.True(savedProject.Identifier.Equals("rnaptap"), "Project identifier is invalid."); - Assert.True(savedProject.Name.Equals("Redmine Net Api Project Test All Properties"), - "Project name is invalid."); - } - - [Fact, Order(2)] - public void Should_Create_Project_With_Parent() - { - var parentProject = - fixture.RedmineManager.CreateObject(new Project {Identifier = "parent-project", Name = "Parent project"}); - - var savedProject = fixture.RedmineManager.CreateObject(CreateTestProjectWithParentSet(parentProject.Id)); - - Assert.NotNull(savedProject); - Assert.True(savedProject.Parent.Id == parentProject.Id, "Parent project is invalid."); - } - - [Fact, Order(3)] - public void Should_Get_Redmine_Net_Api_Project_Test_Project() - { - var project = fixture.RedmineManager.GetObject(PROJECT_IDENTIFIER, null); - - Assert.NotNull(project); - Assert.IsType(project); - Assert.Equal(project.Identifier, PROJECT_IDENTIFIER); - Assert.Equal(project.Name, PROJECT_NAME); - } - - [Fact, Order(4)] - public void Should_Get_Test_Project_With_All_Properties_Set() - { - var project = fixture.RedmineManager.GetObject("rnaptap", new NameValueCollection - { - {RedmineKeys.INCLUDE, string.Join(",", RedmineKeys.TRACKERS, RedmineKeys.ENABLED_MODULES)} - }); - - Assert.NotNull(project); - Assert.IsType(project); - Assert.True(project.Name.Equals("Redmine Net Api Project Test All Properties"), "Project name not equal."); - Assert.True(project.Identifier.Equals("rnaptap"), "Project identifier not equal."); - Assert.True(project.Description.Equals("This is a test project."), "Project description not equal."); - Assert.True(project.HomePage.Equals("www.redminetest.com"), "Project homepage not equal."); - Assert.True(project.IsPublic.Equals(true), - "Project is_public not equal. (This property is available starting with 2.6.0)"); - - Assert.NotNull(project.Trackers); - Assert.True(project.Trackers.Count == 2, "Trackers count != " + 2); - Assert.All(project.Trackers, t => Assert.IsType(t)); - - Assert.NotNull(project.EnabledModules); - Assert.True(project.EnabledModules.Count == 2, - "Enabled modules count (" + project.EnabledModules.Count + ") != " + 2); - Assert.All(project.EnabledModules, em => Assert.IsType(em)); - } - - [Fact, Order(5)] - public void Should_Update_Redmine_Net_Api_Project_Test_Project() - { - const string UPDATED_PROJECT_NAME = "Project created using API updated"; - const string UPDATED_PROJECT_DESCRIPTION = "Test project description updated"; - const string UPDATED_PROJECT_HOMEPAGE = "http://redminetestsupdated.com/"; - const bool UPDATED_PROJECT_ISPUBLIC = true; - const bool UPDATED_PROJECT_INHERIT_MEMBERS = false; - - var project = fixture.RedmineManager.GetObject(PROJECT_IDENTIFIER, null); - project.Name = UPDATED_PROJECT_NAME; - project.Description = UPDATED_PROJECT_DESCRIPTION; - project.HomePage = UPDATED_PROJECT_HOMEPAGE; - project.IsPublic = UPDATED_PROJECT_ISPUBLIC; - project.InheritMembers = UPDATED_PROJECT_INHERIT_MEMBERS; - - var exception = - (RedmineException) - Record.Exception(() => fixture.RedmineManager.UpdateObject(PROJECT_IDENTIFIER, project)); - Assert.Null(exception); - - var updatedProject = fixture.RedmineManager.GetObject(PROJECT_IDENTIFIER, null); - - Assert.True(updatedProject.Name.Equals(UPDATED_PROJECT_NAME), "Project name was not updated."); - Assert.True(updatedProject.Description.Equals(UPDATED_PROJECT_DESCRIPTION), - "Project description was not updated."); - Assert.True(updatedProject.HomePage.Equals(UPDATED_PROJECT_HOMEPAGE), "Project homepage was not updated."); - Assert.True(updatedProject.IsPublic.Equals(UPDATED_PROJECT_ISPUBLIC), - "Project is_public was not updated. (This property is available starting with 2.6.0)"); - } - - [Fact, Order(7)] - public void Should_Throw_Exception_When_Create_Empty_Project() - { - Assert.Throws(() => fixture.RedmineManager.CreateObject(new Project())); - } - - [Fact, Order(8)] - public void Should_Throw_Exception_When_Project_Identifier_Is_Invalid() - { - Assert.Throws(() => fixture.RedmineManager.GetObject("99999999", null)); - } - - [Fact, Order(9)] - public void Should_Delete_Project_And_Parent_Project() - { - var exception = - (RedmineException) Record.Exception(() => fixture.RedmineManager.DeleteObject("rnapwps")); - Assert.Null(exception); - Assert.Throws(() => fixture.RedmineManager.GetObject("rnapwps", null)); - - exception = - (RedmineException) - Record.Exception(() => fixture.RedmineManager.DeleteObject("parent-project")); - Assert.Null(exception); - Assert.Throws(() => fixture.RedmineManager.GetObject("parent-project", null)); - } - - [Fact, Order(10)] - public void Should_Delete_Project_With_All_Properties_Set() - { - var exception = - (RedmineException) Record.Exception(() => fixture.RedmineManager.DeleteObject("rnaptap")); - Assert.Null(exception); - Assert.Throws(() => fixture.RedmineManager.GetObject("rnaptap", null)); - } - - [Fact, Order(11)] - public void Should_Delete_Redmine_Net_Api_Project_Test_Project() - { - var exception = - (RedmineException) - Record.Exception(() => fixture.RedmineManager.DeleteObject(PROJECT_IDENTIFIER)); - Assert.Null(exception); - Assert.Throws(() => fixture.RedmineManager.GetObject(PROJECT_IDENTIFIER, null)); - } - - [Fact, Order(12)] - public void Should_Throw_Exception_Create_Project_Invalid_Trackers() - { - Assert.Throws( - () => fixture.RedmineManager.CreateObject(CreateTestProjectWithInvalidTrackersId())); - } - } -} \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Tests/Sync/QueryTests.cs b/xUnitTest-redmine-net45-api/Tests/Sync/QueryTests.cs deleted file mode 100755 index b9b531af..00000000 --- a/xUnitTest-redmine-net45-api/Tests/Sync/QueryTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using Redmine.Net.Api.Types; -using Xunit; - -namespace xUnitTestredminenet45api -{ - [Trait("Redmine-Net-Api", "Queries")] - [Collection("RedmineCollection")] - public class QueryTests - { - public QueryTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - [Fact, Order(1)] - public void Should_Get_All_Queries() - { - const int NUMBER_OF_QUERIES = 2; - var queries = fixture.RedmineManager.GetObjects(); - - Assert.NotNull(queries); - Assert.All(queries, q => Assert.IsType(q)); - Assert.True(queries.Count == NUMBER_OF_QUERIES, - "Queries count(" + queries.Count + ") != " + NUMBER_OF_QUERIES); - } - } -} \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Tests/Sync/RoleTests.cs b/xUnitTest-redmine-net45-api/Tests/Sync/RoleTests.cs deleted file mode 100755 index 44c030b1..00000000 --- a/xUnitTest-redmine-net45-api/Tests/Sync/RoleTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using Redmine.Net.Api.Types; -using Xunit; - -namespace xUnitTestredminenet45api -{ - [Trait("Redmine-Net-Api", "Roles")] - [Collection("RedmineCollection")] - public class RoleTests - { - public RoleTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - [Fact, Order(1)] - public void Should_Get_All_Roles() - { - const int NUMBER_OF_ROLES = 3; - var roles = fixture.RedmineManager.GetObjects(); - - Assert.NotNull(roles); - Assert.All(roles, r => Assert.IsType(r)); - Assert.True(roles.Count == NUMBER_OF_ROLES, "Roles count(" + roles.Count + ") != " + NUMBER_OF_ROLES); - } - - [Fact, Order(2)] - public void Should_Get_Role_By_Id() - { - const string ROLE_ID = "5"; - const int NUMBER_OF_ROLE_PERMISSIONS = 1; - const string ROLE_NAME = "CustomRole"; - - var role = fixture.RedmineManager.GetObject(ROLE_ID, null); - - Assert.NotNull(role); - Assert.True(role.Name.Equals(ROLE_NAME), "Role name is invalid."); - - Assert.NotNull(role.Permissions); - Assert.All(role.Permissions, p => Assert.IsType(p)); - Assert.True(role.Permissions.Count == NUMBER_OF_ROLE_PERMISSIONS, - "Permissions count(" + role.Permissions.Count + ") != " + NUMBER_OF_ROLE_PERMISSIONS); - } - } -} \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Tests/Sync/TimeEntryActivtiyTests.cs b/xUnitTest-redmine-net45-api/Tests/Sync/TimeEntryActivtiyTests.cs deleted file mode 100755 index 719fe4e4..00000000 --- a/xUnitTest-redmine-net45-api/Tests/Sync/TimeEntryActivtiyTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using Redmine.Net.Api.Types; -using Xunit; - -namespace xUnitTestredminenet45api -{ - [Trait("Redmine-Net-Api", "TimeEntryActivities")] - [Collection("RedmineCollection")] - public class TimeEntryActivityTests - { - public TimeEntryActivityTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - [Fact, Order(1)] - public void Should_Get_All_TimeEntryActivities() - { - const int NUMBER_OF_TIME_ENTRY_ACTIVITIES = 3; - - var timeEntryActivities = fixture.RedmineManager.GetObjects(); - - Assert.NotNull(timeEntryActivities); - Assert.All(timeEntryActivities, t => Assert.IsType(t)); - Assert.True(timeEntryActivities.Count == NUMBER_OF_TIME_ENTRY_ACTIVITIES, - "Time entry activities count ( "+ timeEntryActivities.Count +" ) != " + NUMBER_OF_TIME_ENTRY_ACTIVITIES); - } - } -} \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Tests/Sync/TimeEntryTests.cs b/xUnitTest-redmine-net45-api/Tests/Sync/TimeEntryTests.cs deleted file mode 100755 index fa0019ea..00000000 --- a/xUnitTest-redmine-net45-api/Tests/Sync/TimeEntryTests.cs +++ /dev/null @@ -1,146 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Types; -using Xunit; - -namespace xUnitTestredminenet45api -{ - [Trait("Redmine-Net-Api", "TimeEntries")] - [Collection("RedmineCollection")] - public class TimeEntryTests - { - public TimeEntryTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - [Fact, Order(1)] - public void Should_Create_Time_Entry() - { - const int NEW_TIME_ENTRY_ISSUE_ID = 18; - const int NEW_TIME_ENTRY_PROJECT_ID = 9; - DateTime NEW_TIME_ENTRY_DATE = DateTime.Now; - const int NEW_TIME_ENTRY_HOURS = 1; - const int NEW_TIME_ENTRY_ACTIVITY_ID = 16; - const string NEW_TIME_ENTRY_COMMENTS = "Added time entry on project"; - - var timeEntry = new TimeEntry - { - Issue = new IdentifiableName {Id = NEW_TIME_ENTRY_ISSUE_ID}, - Project = new IdentifiableName {Id = NEW_TIME_ENTRY_PROJECT_ID}, - SpentOn = NEW_TIME_ENTRY_DATE, - Hours = NEW_TIME_ENTRY_HOURS, - Activity = new IdentifiableName {Id = NEW_TIME_ENTRY_ACTIVITY_ID}, - Comments = NEW_TIME_ENTRY_COMMENTS - }; - - var savedTimeEntry = fixture.RedmineManager.CreateObject(timeEntry); - - Assert.NotNull(savedTimeEntry); - Assert.NotNull(savedTimeEntry.Issue); - Assert.True(savedTimeEntry.Issue.Id == NEW_TIME_ENTRY_ISSUE_ID, "Issue id is invalid."); - Assert.NotNull(savedTimeEntry.Project); - Assert.True(savedTimeEntry.Project.Id == NEW_TIME_ENTRY_PROJECT_ID, "Project id is invalid."); - Assert.NotNull(savedTimeEntry.SpentOn); - Assert.True(DateTime.Compare(savedTimeEntry.SpentOn.Value.Date, NEW_TIME_ENTRY_DATE.Date) == 0, - "Date is invalid."); - Assert.NotNull(savedTimeEntry.Hours); - Assert.True(savedTimeEntry.Hours == NEW_TIME_ENTRY_HOURS, "Hours value is not valid."); - Assert.NotNull(savedTimeEntry.Activity); - Assert.True(savedTimeEntry.Activity.Id == NEW_TIME_ENTRY_ACTIVITY_ID, "Activity id is invalid."); - Assert.NotNull(savedTimeEntry.Comments); - Assert.True(savedTimeEntry.Comments.Equals(NEW_TIME_ENTRY_COMMENTS), "Coments value is invalid."); - } - - [Fact, Order(99)] - public void Should_Delete_Time_Entry() - { - const string DELETED_TIME_ENTRY_ID = "43"; - var exception = - (RedmineException) - Record.Exception(() => fixture.RedmineManager.DeleteObject(DELETED_TIME_ENTRY_ID)); - Assert.Null(exception); - Assert.Throws( - () => fixture.RedmineManager.GetObject(DELETED_TIME_ENTRY_ID, null)); - } - - [Fact, Order(2)] - public void Should_Get_All_Time_Entries() - { - var timeEntries = fixture.RedmineManager.GetObjects(); - - Assert.NotNull(timeEntries); - Assert.NotEmpty(timeEntries); - Assert.All(timeEntries, t => Assert.IsType(t)); - } - - [Fact, Order(3)] - public void Should_Get_Time_Entry_By_Id() - { - const string TIME_ENTRY_ID = "30"; - - var timeEntry = fixture.RedmineManager.GetObject(TIME_ENTRY_ID, null); - - Assert.NotNull(timeEntry); - Assert.IsType(timeEntry); - Assert.NotNull(timeEntry.Project); - Assert.NotNull(timeEntry.SpentOn); - Assert.NotNull(timeEntry.Hours); - Assert.NotNull(timeEntry.Activity); - } - - [Fact, Order(4)] - public void Should_Update_Time_Entry() - { - const string UPDATED_TIME_ENTRY_ID = "31"; - const int UPDATED_TIME_ENTRY_ISSUE_ID = 18; - const int UPDATED_TIME_ENTRY_PROJECT_ID = 9; - const int UPDATED_TIME_ENTRY_HOURS = 3; - const int UPDATED_TIME_ENTRY_ACTIVITY_ID = 17; - const string UPDATED_TIME_ENTRY_COMMENTS = "Time entry updated"; - DateTime UPDATED_TIME_ENTRY_DATE = DateTime.Now.AddDays(-2); - - var timeEntry = fixture.RedmineManager.GetObject(UPDATED_TIME_ENTRY_ID, null); - timeEntry.Project.Id = UPDATED_TIME_ENTRY_PROJECT_ID; - timeEntry.Issue.Id = UPDATED_TIME_ENTRY_ISSUE_ID; - timeEntry.SpentOn = UPDATED_TIME_ENTRY_DATE; - timeEntry.Hours = UPDATED_TIME_ENTRY_HOURS; - timeEntry.Comments = UPDATED_TIME_ENTRY_COMMENTS; - - if (timeEntry.Activity == null) timeEntry.Activity = new IdentifiableName(); - timeEntry.Activity.Id = UPDATED_TIME_ENTRY_ACTIVITY_ID; - - fixture.RedmineManager.UpdateObject(UPDATED_TIME_ENTRY_ID, timeEntry); - - var updatedTimeEntry = fixture.RedmineManager.GetObject(UPDATED_TIME_ENTRY_ID, null); - - Assert.NotNull(updatedTimeEntry); - Assert.True(updatedTimeEntry.Project.Id == timeEntry.Project.Id, "Time entry project was not updated."); - Assert.True(updatedTimeEntry.Issue.Id == timeEntry.Issue.Id, "Time entry issue was not updated."); - Assert.True( - updatedTimeEntry.SpentOn != null && timeEntry.SpentOn != null && - DateTime.Compare(updatedTimeEntry.SpentOn.Value.Date, timeEntry.SpentOn.Value.Date) == 0, - "Time entry spent on field was not updated."); - Assert.True(updatedTimeEntry.Hours == timeEntry.Hours, "Time entry hours was not updated."); - Assert.True(updatedTimeEntry.Comments.Equals(timeEntry.Comments), "Time entry comments was not updated."); - } - } -} \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Tests/Sync/TrackerTests.cs b/xUnitTest-redmine-net45-api/Tests/Sync/TrackerTests.cs deleted file mode 100755 index c6f88ceb..00000000 --- a/xUnitTest-redmine-net45-api/Tests/Sync/TrackerTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using Redmine.Net.Api.Types; -using Xunit; - -namespace xUnitTestredminenet45api -{ - [Trait("Redmine-Net-Api", "Trackers")] - [Collection("RedmineCollection")] - public class TrackerTests - { - public TrackerTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - [Fact] - public void RedmineTrackers_ShouldGetAllTrackers() - { - const int NUMBER_OF_TRACKERS = 2; - - var trackers = fixture.RedmineManager.GetObjects(); - - Assert.NotNull(trackers); - Assert.All(trackers, t => Assert.IsType(t)); - Assert.True(trackers.Count == NUMBER_OF_TRACKERS, "Trackers count(" + trackers.Count + ") != " + NUMBER_OF_TRACKERS); - } - } -} \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Tests/Sync/UserTests.cs b/xUnitTest-redmine-net45-api/Tests/Sync/UserTests.cs deleted file mode 100755 index 204d371a..00000000 --- a/xUnitTest-redmine-net45-api/Tests/Sync/UserTests.cs +++ /dev/null @@ -1,215 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System.Collections.Specialized; -using System.Globalization; -using Redmine.Net.Api; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Exceptions; -using Xunit; - -namespace xUnitTestredminenet45api -{ - [Trait("Redmine-Net-Api", "Users")] - [Collection("RedmineCollection")] - [Order(2)] - public class UserTests - { - private readonly RedmineFixture fixture; - - public UserTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private const string USER_LOGIN = "testUser"; - private const string USER_FIRST_NAME = "User"; - private const string USER_LAST_NAME = "One"; - private const string USER_EMAIL = "testUser@mail.com"; - - private static string CREATED_USER_ID; - private static string CREATED_USER_WITH_ALL_PROP_ID; - - private static User CreateTestUserWithRequiredPropertiesSet() - { - var user = new User() - { - Login = USER_LOGIN, - FirstName = USER_FIRST_NAME, - LastName = USER_LAST_NAME, - Email = USER_EMAIL, - }; - - return user; - } - - [Fact, Order(1)] - public void Should_Create_User_With_Required_Properties() - { - var savedUser = fixture.RedmineManager.CreateObject(CreateTestUserWithRequiredPropertiesSet()); - - Assert.NotNull(savedUser); - Assert.NotEqual(savedUser.Id, 0); - - CREATED_USER_ID = savedUser.Id.ToString(); - - Assert.True(savedUser.Login.Equals(USER_LOGIN), "User login is invalid."); - Assert.True(savedUser.FirstName.Equals(USER_FIRST_NAME), "User first name is invalid."); - Assert.True(savedUser.LastName.Equals(USER_LAST_NAME), "User last name is invalid."); - Assert.True(savedUser.Email.Equals(USER_EMAIL), "User email is invalid."); - } - - [Fact, Order(2)] - public void Should_Throw_Exception_When_Create_Empty_User() - { - Assert.Throws(() => fixture.RedmineManager.CreateObject(new User())); - } - - [Fact, Order(3)] - public void Should_Create_User_With_All_Properties_Set() - { - var login = "testUserAllProp"; - var firstName = "firstName"; - var lastName = "lastName"; - var email = "email@a.com"; - var password = "pass123456"; - var mailNotification = "only_assigned"; - - var savedUser = fixture.RedmineManager.CreateObject(new User() - { - Login = login, - FirstName = firstName, - LastName = lastName, - Email = email, - Password = password, - MustChangePassword = true, - }); - - Assert.NotNull(savedUser); - Assert.NotEqual(savedUser.Id, 0); - - CREATED_USER_WITH_ALL_PROP_ID = savedUser.Id.ToString(); - - Assert.True(savedUser.Login.Equals(login), "User login is invalid."); - Assert.True(savedUser.FirstName.Equals(firstName), "User first name is invalid."); - Assert.True(savedUser.LastName.Equals(lastName), "User last name is invalid."); - Assert.True(savedUser.Email.Equals(email), "User email is invalid."); - } - - [Fact, Order(4)] - public void Should_Get_Created_User_With_Required_Fields() - { - var user = fixture.RedmineManager.GetObject(CREATED_USER_ID, null); - - Assert.NotNull(user); - Assert.IsType(user); - Assert.True(user.Login.Equals(USER_LOGIN), "User login is invalid."); - Assert.True(user.FirstName.Equals(USER_FIRST_NAME), "User first name is invalid."); - Assert.True(user.LastName.Equals(USER_LAST_NAME), "User last name is invalid."); - Assert.True(user.Email.Equals(USER_EMAIL), "User email is invalid."); - } - - [Fact, Order(5)] - public void Should_Update_User() - { - const string UPDATED_USER_FIRST_NAME = "UpdatedFirstName"; - const string UPDATED_USER_LAST_NAME = "UpdatedLastName"; - const string UPDATED_USER_EMAIL = "updatedEmail@mail.com"; - - var user = fixture.RedmineManager.GetObject(CREATED_USER_ID, null); - user.FirstName = UPDATED_USER_FIRST_NAME; - user.LastName = UPDATED_USER_LAST_NAME; - user.Email = UPDATED_USER_EMAIL; - - var exception = - (RedmineException) - Record.Exception(() => fixture.RedmineManager.UpdateObject(CREATED_USER_ID, user)); - Assert.Null(exception); - - var updatedUser = fixture.RedmineManager.GetObject(CREATED_USER_ID, null); - - Assert.True(updatedUser.FirstName.Equals(UPDATED_USER_FIRST_NAME), "User first name was not updated."); - Assert.True(updatedUser.LastName.Equals(UPDATED_USER_LAST_NAME), "User last name was not updated."); - Assert.True(updatedUser.Email.Equals(UPDATED_USER_EMAIL), "User email was not updated."); - } - - [Fact, Order(6)] - public void Should_Not_Update_User_With_Invalid_Properties() - { - var user = fixture.RedmineManager.GetObject(CREATED_USER_ID, null); - user.FirstName = ""; - - Assert.Throws(() => fixture.RedmineManager.UpdateObject(CREATED_USER_ID, user)); - } - - [Fact, Order(7)] - public void Should_Delete_User() - { - var exception = - (RedmineException) - Record.Exception(() => fixture.RedmineManager.DeleteObject(CREATED_USER_ID)); - Assert.Null(exception); - Assert.Throws(() => fixture.RedmineManager.GetObject(CREATED_USER_ID, null)); - - } - - [Fact, Order(8)] - public void Should_Delete_User_Created_With_All_Properties_Set() - { - var exception = - (RedmineException) - Record.Exception(() => fixture.RedmineManager.DeleteObject(CREATED_USER_WITH_ALL_PROP_ID)); - Assert.Null(exception); - Assert.Throws(() => fixture.RedmineManager.GetObject(CREATED_USER_WITH_ALL_PROP_ID, null)); - - } - - [Fact, Order(9)] - public void Should_Get_Current_User() - { - User currentUser = fixture.RedmineManager.GetCurrentUser(); - - Assert.NotNull(currentUser); - Assert.Equal(currentUser.ApiKey, Helper.ApiKey); - } - - [Fact, Order(10)] - public void Should_Get_X_Users_From_Offset_Y() - { - var result = fixture.RedmineManager.GetPaginatedObjects(new NameValueCollection() - { - {RedmineKeys.INCLUDE, RedmineKeys.GROUPS + "," + RedmineKeys.MEMBERSHIPS}, - {RedmineKeys.LIMIT, "2"}, - {RedmineKeys.OFFSET, "1"} - }); - - Assert.NotNull(result); - Assert.All(result.Objects, u => Assert.IsType(u)); - } - - [Fact, Order(11)] - public void Should_Get_Users_By_State() - { - var users = fixture.RedmineManager.GetObjects(new NameValueCollection() - { - {RedmineKeys.STATUS, ((int) UserStatus.STATUS_ACTIVE).ToString(CultureInfo.InvariantCulture)} - }); - - Assert.NotNull(users); - Assert.All(users, u => Assert.IsType(u)); - } - } -} \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Tests/Sync/VersionTests.cs b/xUnitTest-redmine-net45-api/Tests/Sync/VersionTests.cs deleted file mode 100755 index 0b3851af..00000000 --- a/xUnitTest-redmine-net45-api/Tests/Sync/VersionTests.cs +++ /dev/null @@ -1,142 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System; -using System.Collections.Specialized; -using Redmine.Net.Api; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Types; -using Xunit; -using Version = Redmine.Net.Api.Types.Version; - -namespace xUnitTestredminenet45api -{ - [Trait("Redmine-Net-Api", "Versions")] - [Collection("RedmineCollection")] - public class VersionTests - { - public VersionTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - private const string PROJECT_ID = "redmine-net-api"; - - [Fact] - [Order(1)] - public void Should_Create_Version() - { - const string NEW_VERSION_NAME = "VersionTesting"; - const VersionStatus NEW_VERSION_STATUS = VersionStatus.locked; - const VersionSharing NEW_VERSION_SHARING = VersionSharing.hierarchy; - DateTime NEW_VERSION_DUE_DATE = DateTime.Now.AddDays(7); - const string NEW_VERSION_DESCRIPTION = "Version description"; - - var version = new Version - { - Name = NEW_VERSION_NAME, - Status = NEW_VERSION_STATUS, - Sharing = NEW_VERSION_SHARING, - DueDate = NEW_VERSION_DUE_DATE, - Description = NEW_VERSION_DESCRIPTION - }; - - var savedVersion = fixture.RedmineManager.CreateObject(version, PROJECT_ID); - - Assert.NotNull(savedVersion); - Assert.NotNull(savedVersion.Project); - Assert.True(savedVersion.Name.Equals(NEW_VERSION_NAME), "Version name is invalid."); - Assert.True(savedVersion.Status.Equals(NEW_VERSION_STATUS), "Version status is invalid."); - Assert.True(savedVersion.Sharing.Equals(NEW_VERSION_SHARING), "Version sharing is invalid."); - Assert.NotNull(savedVersion.DueDate); - Assert.True(savedVersion.DueDate.Value.Date.Equals(NEW_VERSION_DUE_DATE.Date), "Version due date is invalid."); - Assert.True(savedVersion.Description.Equals(NEW_VERSION_DESCRIPTION), "Version description is invalid."); - } - - [Fact] - [Order(99)] - public void Should_Delete_Version() - { - const string DELETED_VERSION_ID = "22"; - var exception = - (RedmineException) - Record.Exception(() => fixture.RedmineManager.DeleteObject(DELETED_VERSION_ID)); - Assert.Null(exception); - Assert.Throws(() => fixture.RedmineManager.GetObject(DELETED_VERSION_ID, null)); - } - - [Fact] - [Order(3)] - public void Should_Get_Version_By_Id() - { - const string VERSION_ID = "6"; - - var version = fixture.RedmineManager.GetObject(VERSION_ID, null); - - Assert.NotNull(version); - } - - [Fact] - [Order(2)] - public void Should_Get_Versions_By_Project_Id() - { - const int NUMBER_OF_VERSIONS = 5; - var versions = - fixture.RedmineManager.GetObjects(new NameValueCollection - { - {RedmineKeys.PROJECT_ID, PROJECT_ID} - }); - - Assert.NotNull(versions); - Assert.All(versions, v => Assert.IsType(v)); - Assert.True(versions.Count == NUMBER_OF_VERSIONS, "Versions count ( "+versions.Count+" ) != " + NUMBER_OF_VERSIONS); - } - - [Fact] - [Order(4)] - public void Should_Update_Version() - { - const string UPDATED_VERSION_ID = "15"; - const string UPDATED_VERSION_NAME = "Updated version"; - const VersionStatus UPDATED_VERSION_STATUS = VersionStatus.closed; - const VersionSharing UPDATED_VERSION_SHARING = VersionSharing.system; - const string UPDATED_VERSION_DESCRIPTION = "Updated description"; - - DateTime UPDATED_VERSION_DUE_DATE = DateTime.Now.AddMonths(1); - - var version = fixture.RedmineManager.GetObject(UPDATED_VERSION_ID, null); - version.Name = UPDATED_VERSION_NAME; - version.Status = UPDATED_VERSION_STATUS; - version.Sharing = UPDATED_VERSION_SHARING; - version.DueDate = UPDATED_VERSION_DUE_DATE; - version.Description = UPDATED_VERSION_DESCRIPTION; - - fixture.RedmineManager.UpdateObject(UPDATED_VERSION_ID, version); - - var updatedVersion = fixture.RedmineManager.GetObject(UPDATED_VERSION_ID, null); - - Assert.NotNull(version); - Assert.True(updatedVersion.Name.Equals(version.Name), "Version name not updated."); - Assert.True(updatedVersion.Status.Equals(version.Status), "Status not updated"); - Assert.True(updatedVersion.Sharing.Equals(version.Sharing), "Sharing not updated"); - Assert.True(updatedVersion.DueDate != null && DateTime.Compare(updatedVersion.DueDate.Value.Date, version.DueDate.Value.Date) == 0, - "DueDate not updated"); - Assert.True(updatedVersion.Description.Equals(version.Description), "Description not updated"); - } - } -} \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/Tests/Sync/WikiPageTests.cs b/xUnitTest-redmine-net45-api/Tests/Sync/WikiPageTests.cs deleted file mode 100755 index a89da164..00000000 --- a/xUnitTest-redmine-net45-api/Tests/Sync/WikiPageTests.cs +++ /dev/null @@ -1,111 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2017 Adrian Popescu. - - 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. -*/ - -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; -using Redmine.Net.Api; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Types; -using Xunit; - -namespace xUnitTestredminenet45api -{ - [Trait("Redmine-Net-Api", "WikiPages")] - [Collection("RedmineCollection")] - public class WikiPageTests - { - public WikiPageTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - private const string PROJECT_ID = "redmine-net-api"; - private const string WIKI_PAGE_NAME = "Wiki"; - - [Fact, Order(1)] - public void Should_Add_Or_Update_WikiPage() - { - const string WIKI_PAGE_UPDATED_TEXT = "Updated again and again wiki page"; - const string WIKI_PAGE_COMMENT = "I did it through code"; - - var page = fixture.RedmineManager.CreateOrUpdateWikiPage(PROJECT_ID, WIKI_PAGE_NAME, - new WikiPage {Text = WIKI_PAGE_UPDATED_TEXT, Comments = WIKI_PAGE_COMMENT}); - - Assert.NotNull(page); - Assert.True(page.Title.Equals(WIKI_PAGE_NAME), "Wiki page name is invalid."); - Assert.True(page.Text.Equals(WIKI_PAGE_UPDATED_TEXT), "Wiki page text is invalid."); - Assert.True(page.Comments.Equals(WIKI_PAGE_COMMENT), "Wiki page comments are invalid."); - } - - [Fact, Order(99)] - public void Should_Delete_Wiki_Page() - { - fixture.RedmineManager.DeleteWikiPage(PROJECT_ID, WIKI_PAGE_NAME); - Assert.Throws(() => fixture.RedmineManager.GetWikiPage(PROJECT_ID, null, WIKI_PAGE_NAME)); - } - - [Fact, Order(2)] - public void Should_Get_All_Wiki_Pages_By_Project_Id() - { - const int NUMBER_OF_WIKI_PAGES = 2; - - var pages = (List) fixture.RedmineManager.GetAllWikiPages(PROJECT_ID); - - Assert.NotNull(pages); - Assert.All(pages, p => Assert.IsType(p)); - Assert.True(pages.Count == NUMBER_OF_WIKI_PAGES, "Wiki pages count != " + NUMBER_OF_WIKI_PAGES); - Assert.True(pages.Exists(p => p.Title == WIKI_PAGE_NAME), - string.Format("Wiki page {0} does not exist", WIKI_PAGE_NAME)); - } - - [Fact, Order(3)] - public void Should_Get_Wiki_Page_By_Title() - { - const string WIKI_PAGE_TITLE = "Wiki2"; - - var page = fixture.RedmineManager.GetWikiPage(PROJECT_ID, null, WIKI_PAGE_TITLE); - - Assert.NotNull(page); - Assert.True(page.Title.Equals(WIKI_PAGE_TITLE), "Wiki page title is invalid."); - } - - [Fact, Order(4)] - public void Should_Get_Wiki_Page_By_Title_With_Attachments() - { - var page = fixture.RedmineManager.GetWikiPage(PROJECT_ID, - new NameValueCollection {{RedmineKeys.INCLUDE, RedmineKeys.ATTACHMENTS}}, WIKI_PAGE_NAME); - - Assert.NotNull(page); - Assert.Equal(page.Title, WIKI_PAGE_NAME); - Assert.NotNull(page.Attachments.ToList()); - Assert.All(page.Attachments.ToList(), a => Assert.IsType(a)); - } - - [Fact, Order(5)] - public void Should_Get_Wiki_Page_By_Version() - { - const int WIKI_PAGE_VERSION = 1; - var oldPage = fixture.RedmineManager.GetWikiPage(PROJECT_ID, null, WIKI_PAGE_NAME, WIKI_PAGE_VERSION); - - Assert.NotNull(oldPage); - Assert.Equal(oldPage.Title, WIKI_PAGE_NAME); - Assert.True(oldPage.Version == WIKI_PAGE_VERSION, "Wiki page version is invalid."); - } - } -} \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/packages.config b/xUnitTest-redmine-net45-api/packages.config deleted file mode 100644 index 4d313c74..00000000 --- a/xUnitTest-redmine-net45-api/packages.config +++ /dev/null @@ -1,10 +0,0 @@ -ο»Ώ - - - - - - - - - \ No newline at end of file diff --git a/xUnitTest-redmine-net45-api/xUnitTest-redmine-net-api.csproj b/xUnitTest-redmine-net45-api/xUnitTest-redmine-net-api.csproj deleted file mode 100644 index 6b1bf606..00000000 --- a/xUnitTest-redmine-net45-api/xUnitTest-redmine-net-api.csproj +++ /dev/null @@ -1,113 +0,0 @@ -ο»Ώ - - - - Debug - AnyCPU - {170210BF-5F03-4531-8A63-06E356CA284B} - Library - xUnitTestredminenet45api - xUnitTest-redmine-net45-api - v4.5.2 - - - - - - true - full - false - bin\Debug - DEBUG;JSON;XML - prompt - 4 - false - - - full - true - bin\Release - prompt - 4 - false - - - Program - ..\packages\xunit.runner.console.2.1.0\tools\xunit.console.exe%24{TargetFile} - false - - - - - - - ..\packages\xunit.abstractions.2.0.1\lib\net35\xunit.abstractions.dll - True - - - ..\packages\xunit.assert.2.2.0\lib\netstandard1.1\xunit.assert.dll - True - - - ..\packages\xunit.extensibility.core.2.2.0\lib\netstandard1.1\xunit.core.dll - True - - - ..\packages\xunit.extensibility.execution.2.2.0\lib\net452\xunit.execution.desktop.dll - True - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4} - redmine-net45-api - - - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - \ No newline at end of file