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 6100a819..1008d646 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,263 @@ -obj/ -bin/ -Test/RedmineManagerTest.cs -Test/Test.csproj -Test/UnitTestRedmine.cs -Test/Properties/AssemblyInfo.cs + +# Created by https://www.gitignore.io/api/visualstudio,visualstudiocode + +### VisualStudio ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + + +### VisualStudioCode ### +.vscode + +.artifacts diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8052bae9..00000000 --- a/.travis.yml +++ /dev/null @@ -1,2 +0,0 @@ -language: csharp -solution: redmine-net-api.sln \ No newline at end of file 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 new file mode 100755 index 00000000..4613d15a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,8 @@ +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. 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/LICENSE b/LICENSE new file mode 100755 index 00000000..5c304d1a --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. 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 old mode 100644 new mode 100755 index 5b104d89..9a2add82 --- a/README.md +++ b/README.md @@ -1,56 +1,119 @@ -# 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, update, delete) for the resources described below - * Attachments - * Custom Fields - * Enumerations - * Groups - * Issues - * Issue Categories - * Issue Relations - * Issue Statuses - * News(implementation for index only) - * Projects - * Project Memberships - * Queries - * Roles - * Time Entries - * Trackers - * Users - * Versions - * Wiki Pages - -**Authentication** - -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. - -**User Impersonation** - -As of Redmine 2.2.0, you can impersonate user through the REST API by setting the X-Redmine-Switch-User header of your API request. It must be set to a user login (eg. X-Redmine-Switch-User: 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. - -If the login specified with the X-Redmine-Switch-User header does not exist or is not active, you will receive a 412 error response. - - -
-## Help me help you ## -Your feedback is crucial. If you find anything that could improve the API, let me know. I will gladly receive your input and make the proper adjustments. - -**If you find this API useful let others know about it and/or rate it.**
-Your contribution is always welcome. - -
-## Licence ## -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. +# ![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/Test/Properties/AssemblyInfo.cs b/Test/Properties/AssemblyInfo.cs deleted file mode 100644 index a3b532f6..00000000 --- a/Test/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("Test")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Test")] -[assembly: AssemblyCopyright("Copyright Β© 2015")] -[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("f058d852-c7e5-4507-ad6b-3b631690ea44")] - -// 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/UnitTestRedmineNetApi/App.config b/UnitTestRedmineNetApi/App.config deleted file mode 100644 index 08a0e0d3..00000000 --- a/UnitTestRedmineNetApi/App.config +++ /dev/null @@ -1,7 +0,0 @@ -ο»Ώ - - - - - - \ No newline at end of file diff --git a/UnitTestRedmineNetApi/GroupTests.cs b/UnitTestRedmineNetApi/GroupTests.cs deleted file mode 100644 index 2d8de4b4..00000000 --- a/UnitTestRedmineNetApi/GroupTests.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Configuration; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Redmine.Net.Api; -using Redmine.Net.Api.Types; - -namespace UnitTestRedmineNetApi -{ - [TestClass] - public class GroupTests - { - private RedmineManager redmineManager; - - [TestInitialize] - public void Initialize() - { - var uri = ConfigurationManager.AppSettings["uri"]; - var apiKey = ConfigurationManager.AppSettings["apiKey"]; - redmineManager = new RedmineManager(uri, apiKey); - } - - [TestMethod] - public void GetAllGroups() - { - var result = redmineManager.GetObjectList(null); - - Assert.IsNotNull(result); - } - - [TestMethod] - public void GetGroup_With_Memberships() - { - var result = redmineManager.GetObject("9", new NameValueCollection() { { "include", "memberships" } }); - - Assert.IsNotNull(result); - } - - [TestMethod] - public void Should_Return_Group_With_Users() - { - var result = redmineManager.GetObject("9", new NameValueCollection() { { "include", "users" } }); - - Assert.IsNotNull(result); - } - - [TestMethod] - public void GetGroup_WithAll_AssociatedData() - { - var result = redmineManager.GetObject("9", new NameValueCollection() { { "include", "memberships, users" } }); - - Assert.IsNotNull(result); - } - - [TestMethod] - public void Add_New_Membership_To_Group() - { - var group = redmineManager.GetObject("9", new NameValueCollection() { { "include", "memberships" } }); - - var mbs = new Membership(); - mbs.Roles = new List(); - mbs.Roles.Add(new MembershipRole() - { - Inherited = true, - Name = "role de test" - }); - - group.Memberships.Add(mbs); - - redmineManager.UpdateObject("9", group); - - var updatedGroup = redmineManager.GetObject("9", new NameValueCollection() { { "include", "memberships" } }); - } - - } -} \ No newline at end of file diff --git a/UnitTestRedmineNetApi/IssueCategoriesTests.cs b/UnitTestRedmineNetApi/IssueCategoriesTests.cs deleted file mode 100644 index 11c6e213..00000000 --- a/UnitTestRedmineNetApi/IssueCategoriesTests.cs +++ /dev/null @@ -1,22 +0,0 @@ -ο»Ώusing System.Configuration; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Redmine.Net.Api; - -namespace UnitTestRedmineNetApi -{ - [TestClass] - public class IssueCategoriesTests - { - private RedmineManager redmineManager; - - [TestInitialize] - public void Initialize() - { - var uri = ConfigurationManager.AppSettings["uri"]; - var apiKey = ConfigurationManager.AppSettings["apiKey"]; - redmineManager = new RedmineManager(uri, apiKey); - } - - - } -} \ No newline at end of file diff --git a/UnitTestRedmineNetApi/MembershipTests.cs b/UnitTestRedmineNetApi/MembershipTests.cs deleted file mode 100644 index 30b2a8fb..00000000 --- a/UnitTestRedmineNetApi/MembershipTests.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Configuration; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Redmine.Net.Api; - -namespace UnitTestRedmineNetApi -{ - [TestClass] - public class MembershipTests - { - private RedmineManager redmineManager; - - [TestInitialize] - public void Initialize() - { - var uri = ConfigurationManager.AppSettings["uri"]; - var apiKey = ConfigurationManager.AppSettings["apiKey"]; - redmineManager = new RedmineManager(uri, apiKey); - } - - [TestMethod] - public void GetAllMemberships() - { - - } - } -} \ No newline at end of file diff --git a/UnitTestRedmineNetApi/NewsTests.cs b/UnitTestRedmineNetApi/NewsTests.cs deleted file mode 100644 index 3053181d..00000000 --- a/UnitTestRedmineNetApi/NewsTests.cs +++ /dev/null @@ -1,31 +0,0 @@ -ο»Ώusing System; -using System.Configuration; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Redmine.Net.Api; -using Redmine.Net.Api.Types; - -namespace UnitTestRedmineNetApi -{ - [TestClass] - public class NewsTests - { - private RedmineManager redmineManager; - - [TestInitialize] - public void Initialize() - { - var uri = ConfigurationManager.AppSettings["uri"]; - var apiKey = ConfigurationManager.AppSettings["apiKey"]; - redmineManager = new RedmineManager(uri, apiKey); - } - - - [TestMethod] - public void GetAllNews() - { - var result = redmineManager.GetObjectList(null); - - Assert.IsNotNull(result); - } - } -} diff --git a/UnitTestRedmineNetApi/ProjectTests.cs b/UnitTestRedmineNetApi/ProjectTests.cs deleted file mode 100644 index ca4af322..00000000 --- a/UnitTestRedmineNetApi/ProjectTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -ο»Ώusing System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Configuration; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Redmine.Net.Api; -using Redmine.Net.Api.Types; - -namespace UnitTestRedmineNetApi -{ - [TestClass] - public class ProjectTests - { - private RedmineManager redmineManager; - - [TestInitialize] - public void Initialize() - { - var uri = ConfigurationManager.AppSettings["uri"]; - var apiKey = ConfigurationManager.AppSettings["apiKey"]; - redmineManager = new RedmineManager(uri, apiKey); - } - - - [TestMethod] - public void GetProject_WithAll_AssociatedData() - { - var result = redmineManager.GetObject("9", new NameValueCollection() - { - {"include","trackers, issue_categories, enabled_modules" } - }); - - Assert.IsNotNull(result); - Assert.IsInstanceOfType(result, typeof(Project)); - Assert.IsNotNull(result.Trackers, "result.Trackers != null"); - Assert.IsNotNull(result.IssueCategories, "result.IssueCategories != null"); - Assert.IsNotNull(result.EnabledModules,"result.EnabledModules != null"); - } - - [TestMethod] - public void GetAllProjects_WithAll_AssociatedData() - { - IList result = redmineManager.GetTotalObjectList(new NameValueCollection() - { - {"include", "trackers, issue_categories, enabled_modules"} - }); - - Assert.IsNotNull(result); - } - - [TestMethod] - public void GetProject_News() - { - var result = redmineManager.GetObjectList(new NameValueCollection() - { - {"project_id","9" } - }); - - Assert.IsNotNull(result); - } - } -} diff --git a/UnitTestRedmineNetApi/Properties/AssemblyInfo.cs b/UnitTestRedmineNetApi/Properties/AssemblyInfo.cs deleted file mode 100644 index b28d3a20..00000000 --- a/UnitTestRedmineNetApi/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("UnitTestRedmineNetApi")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("UnitTestRedmineNetApi")] -[assembly: AssemblyCopyright("Copyright Β© 2015")] -[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("0de30921-1273-41f6-91d6-ab17ec61e8f0")] - -// 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/UnitTestRedmineNetApi/UnitTestRedmineNetApi.csproj b/UnitTestRedmineNetApi/UnitTestRedmineNetApi.csproj deleted file mode 100644 index 78a97b23..00000000 --- a/UnitTestRedmineNetApi/UnitTestRedmineNetApi.csproj +++ /dev/null @@ -1,104 +0,0 @@ -ο»Ώ - - - Debug - AnyCPU - {0DE30921-1273-41F6-91D6-AB17EC61E8F0} - Library - Properties - UnitTestRedmineNetApi - UnitTestRedmineNetApi - v4.5 - 512 - {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - 10.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages - False - UnitTest - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - False - - - - - - - - - - - - - - - - - - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E} - redmine-net40-api - - - - - - - - - - False - - - False - - - False - - - False - - - - - - - - \ No newline at end of file diff --git a/UnitTestRedmineNetApi/UserTestsJson.cs b/UnitTestRedmineNetApi/UserTestsJson.cs deleted file mode 100644 index f5d07829..00000000 --- a/UnitTestRedmineNetApi/UserTestsJson.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Configuration; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Redmine.Net.Api; - -namespace UnitTestRedmineNetApi -{ - [TestClass] - public class UserTestsJson - { - private RedmineManager redmineManager; - - [TestInitialize] - public void Initialize() - { - var uri = ConfigurationManager.AppSettings["uri"]; - var apiKey = ConfigurationManager.AppSettings["apiKey"]; - redmineManager = new RedmineManager(uri, apiKey, MimeFormat.json); - } - - [TestMethod] - public void Should_Add_User() - { - redmineManager.AddUser(44, 8); - Assert.Inconclusive(); - } - - [TestMethod] - public void Should_Remove_User() - { - redmineManager.DeleteUser(44, 8); - Assert.Inconclusive(); - } - - [TestMethod] - public void Should_Return_Current_User() - { - var result = redmineManager.GetCurrentUser(); - Assert.Inconclusive(); - } - - [TestMethod] - public void Should_Return_All_Users() - { - var result = redmineManager.GetUsers(); - Assert.Inconclusive(); - } - } -} \ No newline at end of file diff --git a/UnitTestRedmineNetApi/WatcherTestsJson.cs b/UnitTestRedmineNetApi/WatcherTestsJson.cs deleted file mode 100644 index 9b3285ca..00000000 --- a/UnitTestRedmineNetApi/WatcherTestsJson.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Configuration; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Redmine.Net.Api; - -namespace UnitTestRedmineNetApi -{ - [TestClass] - public class WatcherTestsJson - { - private RedmineManager redmineManager; - - [TestInitialize] - public void Initialize() - { - var uri = ConfigurationManager.AppSettings["uri"]; - var apiKey = ConfigurationManager.AppSettings["apiKey"]; - redmineManager = new RedmineManager(uri, apiKey, MimeFormat.json); - } - - public void Should_Add_Watcher() - { - redmineManager.AddWatcher(44, 8); - Assert.Inconclusive(); - } - - [TestMethod] - public void Should_Remove_Watcher() - { - redmineManager.RemoveWatcher(44, 8); - Assert.Inconclusive(); - } - } -} \ No newline at end of file diff --git a/UnitTestRedmineNetApi/WikiTestsJson.cs b/UnitTestRedmineNetApi/WikiTestsJson.cs deleted file mode 100644 index b7e924da..00000000 --- a/UnitTestRedmineNetApi/WikiTestsJson.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Collections.Specialized; -using System.Configuration; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Redmine.Net.Api; -using Redmine.Net.Api.Types; - -namespace UnitTestRedmineNetApi -{ - [TestClass] - public class WikiTestsJson - { - private RedmineManager redmineManager; - - [TestInitialize] - public void Initialize() - { - var uri = ConfigurationManager.AppSettings["uri"]; - var apiKey = ConfigurationManager.AppSettings["apiKey"]; - redmineManager = new RedmineManager(uri, apiKey, MimeFormat.xml); - } - - [TestMethod] - public void Should_Add_Wiki() - { - var result = redmineManager.CreateOrUpdateWikiPage("", "", new WikiPage()); - Assert.Inconclusive(); - } - - [TestMethod] - public void Should_Remove_Wiki() - { - redmineManager.DeleteWikiPage("", ""); - Assert.Inconclusive(); - } - - [TestMethod] - public void Should_Update_Wiki() - { - var wiki = GetWiki("9", null, "Wiki", 2); - - Assert.IsNotNull(wiki,"wiki != null"); - - wiki.Text = "text updated"; - wiki.Comments = "comments updated"; - wiki.Title = "am schimbat titlul"; - - redmineManager.CreateOrUpdateWikiPage("9", "Wiki", wiki); - - Assert.Inconclusive(); - } - - [TestMethod] - public void Should_Get_Wiki() - { - var result = GetWiki("9", null, "Wiki", 0); - Assert.Inconclusive(); - } - - [TestMethod] - public void Should_Get_All_Wikis() - { - var result = redmineManager.GetAllWikiPages("9"); - Assert.Inconclusive(); - } - - private WikiPage GetWiki(string projectId, NameValueCollection parameters, string pageName, uint version) - { - return redmineManager.GetWikiPage(projectId, parameters, pageName, version); - } - } -} \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..8fc91681 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,88 @@ +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: true + +nuget: + disable_publish_on_pr: true + +branches: + only: + - master + - /\d*\.\d*\.\d*/ + +init: + # Good practise, because Windows line endings are different from Unix/Linux ones + - ps: git config --global core.autocrlf true + + - 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"}); + +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: 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: + - 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/fonts/OpenSans-Bold-webfont.eot b/fonts/OpenSans-Bold-webfont.eot deleted file mode 100644 index e1c76744..00000000 Binary files a/fonts/OpenSans-Bold-webfont.eot and /dev/null differ diff --git a/fonts/OpenSans-Bold-webfont.svg b/fonts/OpenSans-Bold-webfont.svg deleted file mode 100644 index 364b3686..00000000 --- a/fonts/OpenSans-Bold-webfont.svg +++ /dev/null @@ -1,146 +0,0 @@ - - - - -This is a custom SVG webfont generated by Font Squirrel. -Copyright : Digitized data copyright 20102011 Google Corporation -Foundry : Ascender Corporation -Foundry URL : httpwwwascendercorpcom - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/fonts/OpenSans-Bold-webfont.ttf b/fonts/OpenSans-Bold-webfont.ttf deleted file mode 100644 index 2d94f062..00000000 Binary files a/fonts/OpenSans-Bold-webfont.ttf and /dev/null differ diff --git a/fonts/OpenSans-Bold-webfont.woff b/fonts/OpenSans-Bold-webfont.woff deleted file mode 100644 index cd86852d..00000000 Binary files a/fonts/OpenSans-Bold-webfont.woff and /dev/null differ diff --git a/fonts/OpenSans-BoldItalic-webfont.eot b/fonts/OpenSans-BoldItalic-webfont.eot deleted file mode 100644 index f44ac9a3..00000000 Binary files a/fonts/OpenSans-BoldItalic-webfont.eot and /dev/null differ diff --git a/fonts/OpenSans-BoldItalic-webfont.svg b/fonts/OpenSans-BoldItalic-webfont.svg deleted file mode 100644 index 8392240a..00000000 --- a/fonts/OpenSans-BoldItalic-webfont.svg +++ /dev/null @@ -1,146 +0,0 @@ - - - - -This is a custom SVG webfont generated by Font Squirrel. -Copyright : Digitized data copyright 20102011 Google Corporation -Foundry : Ascender Corporation -Foundry URL : httpwwwascendercorpcom - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/fonts/OpenSans-BoldItalic-webfont.ttf b/fonts/OpenSans-BoldItalic-webfont.ttf deleted file mode 100644 index f74e0e3c..00000000 Binary files a/fonts/OpenSans-BoldItalic-webfont.ttf and /dev/null differ diff --git a/fonts/OpenSans-BoldItalic-webfont.woff b/fonts/OpenSans-BoldItalic-webfont.woff deleted file mode 100644 index f3248c11..00000000 Binary files a/fonts/OpenSans-BoldItalic-webfont.woff and /dev/null differ diff --git a/fonts/OpenSans-Italic-webfont.eot b/fonts/OpenSans-Italic-webfont.eot deleted file mode 100644 index 277c1899..00000000 Binary files a/fonts/OpenSans-Italic-webfont.eot and /dev/null differ diff --git a/fonts/OpenSans-Italic-webfont.svg b/fonts/OpenSans-Italic-webfont.svg deleted file mode 100644 index 29c7497f..00000000 --- a/fonts/OpenSans-Italic-webfont.svg +++ /dev/null @@ -1,146 +0,0 @@ - - - - -This is a custom SVG webfont generated by Font Squirrel. -Copyright : Digitized data copyright 20102011 Google Corporation -Foundry : Ascender Corporation -Foundry URL : httpwwwascendercorpcom - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/fonts/OpenSans-Italic-webfont.ttf b/fonts/OpenSans-Italic-webfont.ttf deleted file mode 100644 index 63f187e9..00000000 Binary files a/fonts/OpenSans-Italic-webfont.ttf and /dev/null differ diff --git a/fonts/OpenSans-Italic-webfont.woff b/fonts/OpenSans-Italic-webfont.woff deleted file mode 100644 index 469a29bb..00000000 Binary files a/fonts/OpenSans-Italic-webfont.woff and /dev/null differ diff --git a/fonts/OpenSans-Light-webfont.eot b/fonts/OpenSans-Light-webfont.eot deleted file mode 100644 index 837daab8..00000000 Binary files a/fonts/OpenSans-Light-webfont.eot and /dev/null differ diff --git a/fonts/OpenSans-Light-webfont.svg b/fonts/OpenSans-Light-webfont.svg deleted file mode 100644 index bdb67265..00000000 --- a/fonts/OpenSans-Light-webfont.svg +++ /dev/null @@ -1,146 +0,0 @@ - - - - -This is a custom SVG webfont generated by Font Squirrel. -Copyright : Digitized data copyright 20102011 Google Corporation -Foundry : Ascender Corporation -Foundry URL : httpwwwascendercorpcom - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/fonts/OpenSans-Light-webfont.ttf b/fonts/OpenSans-Light-webfont.ttf deleted file mode 100644 index b50ef9dc..00000000 Binary files a/fonts/OpenSans-Light-webfont.ttf and /dev/null differ diff --git a/fonts/OpenSans-Light-webfont.woff b/fonts/OpenSans-Light-webfont.woff deleted file mode 100644 index 99514d1a..00000000 Binary files a/fonts/OpenSans-Light-webfont.woff and /dev/null differ diff --git a/fonts/OpenSans-LightItalic-webfont.eot b/fonts/OpenSans-LightItalic-webfont.eot deleted file mode 100644 index f0ebf2c0..00000000 Binary files a/fonts/OpenSans-LightItalic-webfont.eot and /dev/null differ diff --git a/fonts/OpenSans-LightItalic-webfont.svg b/fonts/OpenSans-LightItalic-webfont.svg deleted file mode 100644 index 60765da8..00000000 --- a/fonts/OpenSans-LightItalic-webfont.svg +++ /dev/null @@ -1,146 +0,0 @@ - - - - -This is a custom SVG webfont generated by Font Squirrel. -Copyright : Digitized data copyright 20102011 Google Corporation -Foundry : Ascender Corporation -Foundry URL : httpwwwascendercorpcom - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/fonts/OpenSans-LightItalic-webfont.ttf b/fonts/OpenSans-LightItalic-webfont.ttf deleted file mode 100644 index 5898c8c7..00000000 Binary files a/fonts/OpenSans-LightItalic-webfont.ttf and /dev/null differ diff --git a/fonts/OpenSans-LightItalic-webfont.woff b/fonts/OpenSans-LightItalic-webfont.woff deleted file mode 100644 index 9c978dc3..00000000 Binary files a/fonts/OpenSans-LightItalic-webfont.woff and /dev/null differ diff --git a/fonts/OpenSans-Regular-webfont.eot b/fonts/OpenSans-Regular-webfont.eot deleted file mode 100644 index dd6fd2cb..00000000 Binary files a/fonts/OpenSans-Regular-webfont.eot and /dev/null differ diff --git a/fonts/OpenSans-Regular-webfont.svg b/fonts/OpenSans-Regular-webfont.svg deleted file mode 100644 index 01038bb1..00000000 --- a/fonts/OpenSans-Regular-webfont.svg +++ /dev/null @@ -1,146 +0,0 @@ - - - - -This is a custom SVG webfont generated by Font Squirrel. -Copyright : Digitized data copyright 20102011 Google Corporation -Foundry : Ascender Corporation -Foundry URL : httpwwwascendercorpcom - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/fonts/OpenSans-Regular-webfont.ttf b/fonts/OpenSans-Regular-webfont.ttf deleted file mode 100644 index 05951e7b..00000000 Binary files a/fonts/OpenSans-Regular-webfont.ttf and /dev/null differ diff --git a/fonts/OpenSans-Regular-webfont.woff b/fonts/OpenSans-Regular-webfont.woff deleted file mode 100644 index 274664b2..00000000 Binary files a/fonts/OpenSans-Regular-webfont.woff and /dev/null differ diff --git a/fonts/OpenSans-Semibold-webfont.eot b/fonts/OpenSans-Semibold-webfont.eot deleted file mode 100644 index 289aade3..00000000 Binary files a/fonts/OpenSans-Semibold-webfont.eot and /dev/null differ diff --git a/fonts/OpenSans-Semibold-webfont.svg b/fonts/OpenSans-Semibold-webfont.svg deleted file mode 100644 index cc2ca427..00000000 --- a/fonts/OpenSans-Semibold-webfont.svg +++ /dev/null @@ -1,146 +0,0 @@ - - - - -This is a custom SVG webfont generated by Font Squirrel. -Copyright : Digitized data copyright 2011 Google Corporation -Foundry : Ascender Corporation -Foundry URL : httpwwwascendercorpcom - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/fonts/OpenSans-Semibold-webfont.ttf b/fonts/OpenSans-Semibold-webfont.ttf deleted file mode 100644 index 6f150731..00000000 Binary files a/fonts/OpenSans-Semibold-webfont.ttf and /dev/null differ diff --git a/fonts/OpenSans-Semibold-webfont.woff b/fonts/OpenSans-Semibold-webfont.woff deleted file mode 100644 index 4e47cb1a..00000000 Binary files a/fonts/OpenSans-Semibold-webfont.woff and /dev/null differ diff --git a/fonts/OpenSans-SemiboldItalic-webfont.eot b/fonts/OpenSans-SemiboldItalic-webfont.eot deleted file mode 100644 index 50a8a6f7..00000000 Binary files a/fonts/OpenSans-SemiboldItalic-webfont.eot and /dev/null differ diff --git a/fonts/OpenSans-SemiboldItalic-webfont.svg b/fonts/OpenSans-SemiboldItalic-webfont.svg deleted file mode 100644 index 65b50e2a..00000000 --- a/fonts/OpenSans-SemiboldItalic-webfont.svg +++ /dev/null @@ -1,146 +0,0 @@ - - - - -This is a custom SVG webfont generated by Font Squirrel. -Copyright : Digitized data copyright 20102011 Google Corporation -Foundry : Ascender Corporation -Foundry URL : httpwwwascendercorpcom - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/fonts/OpenSans-SemiboldItalic-webfont.ttf b/fonts/OpenSans-SemiboldItalic-webfont.ttf deleted file mode 100644 index 55ba3120..00000000 Binary files a/fonts/OpenSans-SemiboldItalic-webfont.ttf and /dev/null differ diff --git a/fonts/OpenSans-SemiboldItalic-webfont.woff b/fonts/OpenSans-SemiboldItalic-webfont.woff deleted file mode 100644 index 0adc6df1..00000000 Binary files a/fonts/OpenSans-SemiboldItalic-webfont.woff and /dev/null differ 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/images/bullet.png b/images/bullet.png deleted file mode 100644 index 0614eb65..00000000 Binary files a/images/bullet.png and /dev/null differ diff --git a/images/hr.gif b/images/hr.gif deleted file mode 100644 index bdb4168d..00000000 Binary files a/images/hr.gif and /dev/null differ diff --git a/images/nav-bg.gif b/images/nav-bg.gif deleted file mode 100644 index 47439656..00000000 Binary files a/images/nav-bg.gif and /dev/null differ diff --git a/index.html b/index.html deleted file mode 100644 index 111ae136..00000000 --- a/index.html +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - Redmine-net-api by zapadi - - - - - - - - - - - - -
- -
-
-

Redmine-net-api

-

redmine-net-api

-
- Project maintained by zapadi - Hosted on GitHub Pages — Theme by mattgraham -
- -

-redmine-net-api

- -

redmine-net-api is a library for communicating with a Redmine project management application

- -
    -
  • Uses Redmine's 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, update, delete) for the resources described below - -
      -
    • Attachments
    • -
    • Custom Fields
    • -
    • Enumerations
      -
    • -
    • Groups
    • -
    • Issues
      -
    • -
    • Issue Categories
    • -
    • Issue Relations
    • -
    • Issue Statuses
    • -
    • News(implementation for index only)
    • -
    • Projects
    • -
    • Project Memberships
    • -
    • Queries
      -
    • -
    • Roles
    • -
    • Time Entries
    • -
    • Trackers
    • -
    • Users
    • -
    • Versions
    • -
    • Wiki Pages
    • -
    -
  • -
- -

Authentication

- -

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.
  • -
- -

User Impersonation

- -

As of Redmine 2.2.0, you can impersonate user through the REST API by setting the X-Redmine-Switch-User header of your API request. It must be set to a user login (eg. X-Redmine-Switch-User: 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.

- -

If the login specified with the X-Redmine-Switch-User header does not exist or is not active, you will receive a 412 error response.

- -


- -

-Help me help you

- -

Your feedback is crucial. If you find anything that could improve the API, let me know. I will gladly receive your input and make the proper adjustments.

- -

If you find this API useful let others know about it and/or rate it.
-Your contribution is always welcome.

- -


- -

-Licence

- -

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.

-
- -
- - - - diff --git a/javascripts/respond.js b/javascripts/respond.js deleted file mode 100644 index 76bc2604..00000000 --- a/javascripts/respond.js +++ /dev/null @@ -1,779 +0,0 @@ -if(typeof Object.create!=="function"){ -Object.create=function(o){ -function F(){ -}; -F.prototype=o; -return new F(); -}; -} -var ua={toString:function(){ -return navigator.userAgent; -},test:function(s){ -return this.toString().toLowerCase().indexOf(s.toLowerCase())>-1; -}}; -ua.version=(ua.toString().toLowerCase().match(/[\s\S]+(?:rv|it|ra|ie)[\/: ]([\d.]+)/)||[])[1]; -ua.webkit=ua.test("webkit"); -ua.gecko=ua.test("gecko")&&!ua.webkit; -ua.opera=ua.test("opera"); -ua.ie=ua.test("msie")&&!ua.opera; -ua.ie6=ua.ie&&document.compatMode&&typeof document.documentElement.style.maxHeight==="undefined"; -ua.ie7=ua.ie&&document.documentElement&&typeof document.documentElement.style.maxHeight!=="undefined"&&typeof XDomainRequest==="undefined"; -ua.ie8=ua.ie&&typeof XDomainRequest!=="undefined"; -var domReady=function(){ -var _1=[]; -var _2=function(){ -if(!arguments.callee.done){ -arguments.callee.done=true; -for(var i=0;i<_1.length;i++){ -_1[i](); -} -} -}; -if(document.addEventListener){ -document.addEventListener("DOMContentLoaded",_2,false); -} -if(ua.ie){ -(function(){ -try{ -document.documentElement.doScroll("left"); -} -catch(e){ -setTimeout(arguments.callee,50); -return; -} -_2(); -})(); -document.onreadystatechange=function(){ -if(document.readyState==="complete"){ -document.onreadystatechange=null; -_2(); -} -}; -} -if(ua.webkit&&document.readyState){ -(function(){ -if(document.readyState!=="loading"){ -_2(); -}else{ -setTimeout(arguments.callee,10); -} -})(); -} -window.onload=_2; -return function(fn){ -if(typeof fn==="function"){ -_1[_1.length]=fn; -} -return fn; -}; -}(); -var cssHelper=function(){ -var _3={BLOCKS:/[^\s{][^{]*\{(?:[^{}]*\{[^{}]*\}[^{}]*|[^{}]*)*\}/g,BLOCKS_INSIDE:/[^\s{][^{]*\{[^{}]*\}/g,DECLARATIONS:/[a-zA-Z\-]+[^;]*:[^;]+;/g,RELATIVE_URLS:/url\(['"]?([^\/\)'"][^:\)'"]+)['"]?\)/g,REDUNDANT_COMPONENTS:/(?:\/\*([^*\\\\]|\*(?!\/))+\*\/|@import[^;]+;)/g,REDUNDANT_WHITESPACE:/\s*(,|:|;|\{|\})\s*/g,MORE_WHITESPACE:/\s{2,}/g,FINAL_SEMICOLONS:/;\}/g,NOT_WHITESPACE:/\S+/g}; -var _4,_5=false; -var _6=[]; -var _7=function(fn){ -if(typeof fn==="function"){ -_6[_6.length]=fn; -} -}; -var _8=function(){ -for(var i=0;i<_6.length;i++){ -_6[i](_4); -} -}; -var _9={}; -var _a=function(n,v){ -if(_9[n]){ -var _b=_9[n].listeners; -if(_b){ -for(var i=0;i<_b.length;i++){ -_b[i](v); -} -} -} -}; -var _c=function(_d,_e,_f){ -if(ua.ie&&!window.XMLHttpRequest){ -window.XMLHttpRequest=function(){ -return new ActiveXObject("Microsoft.XMLHTTP"); -}; -} -if(!XMLHttpRequest){ -return ""; -} -var r=new XMLHttpRequest(); -try{ -r.open("get",_d,true); -r.setRequestHeader("X_REQUESTED_WITH","XMLHttpRequest"); -} -catch(e){ -_f(); -return; -} -var _10=false; -setTimeout(function(){ -_10=true; -},5000); -document.documentElement.style.cursor="progress"; -r.onreadystatechange=function(){ -if(r.readyState===4&&!_10){ -if(!r.status&&location.protocol==="file:"||(r.status>=200&&r.status<300)||r.status===304||navigator.userAgent.indexOf("Safari")>-1&&typeof r.status==="undefined"){ -_e(r.responseText); -}else{ -_f(); -} -document.documentElement.style.cursor=""; -r=null; -} -}; -r.send(""); -}; -var _11=function(_12){ -_12=_12.replace(_3.REDUNDANT_COMPONENTS,""); -_12=_12.replace(_3.REDUNDANT_WHITESPACE,"$1"); -_12=_12.replace(_3.MORE_WHITESPACE," "); -_12=_12.replace(_3.FINAL_SEMICOLONS,"}"); -return _12; -}; -var _13={mediaQueryList:function(s){ -var o={}; -var idx=s.indexOf("{"); -var lt=s.substring(0,idx); -s=s.substring(idx+1,s.length-1); -var mqs=[],rs=[]; -var qts=lt.toLowerCase().substring(7).split(","); -for(var i=0;i-1&&_23.href&&_23.href.length!==0&&!_23.disabled){ -_1f[_1f.length]=_23; -} -} -if(_1f.length>0){ -var c=0; -var _24=function(){ -c++; -if(c===_1f.length){ -_20(); -} -}; -var _25=function(_26){ -var _27=_26.href; -_c(_27,function(_28){ -_28=_11(_28).replace(_3.RELATIVE_URLS,"url("http://github.com/+_27.substring(0,_27.lastIndexOf(%22/"))+"/$1)"); -_26.cssHelperText=_28; -_24(); -},_24); -}; -for(i=0;i<_1f.length;i++){ -_25(_1f[i]); -} -}else{ -_20(); -} -}; -var _29={mediaQueryLists:"array",rules:"array",selectors:"object",declarations:"array",properties:"object"}; -var _2a={mediaQueryLists:null,rules:null,selectors:null,declarations:null,properties:null}; -var _2b=function(_2c,v){ -if(_2a[_2c]!==null){ -if(_29[_2c]==="array"){ -return (_2a[_2c]=_2a[_2c].concat(v)); -}else{ -var c=_2a[_2c]; -for(var n in v){ -if(v.hasOwnProperty(n)){ -if(!c[n]){ -c[n]=v[n]; -}else{ -c[n]=c[n].concat(v[n]); -} -} -} -return c; -} -} -}; -var _2d=function(_2e){ -_2a[_2e]=(_29[_2e]==="array")?[]:{}; -for(var i=0;i<_4.length;i++){ -_2b(_2e,_4[i].cssHelperParsed[_2e]); -} -return _2a[_2e]; -}; -domReady(function(){ -var els=document.body.getElementsByTagName("*"); -for(var i=0;i=_44)||(max&&_46<_44)||(!min&&!max&&_46===_44)); -}else{ -return false; -} -}else{ -return _46>0; -} -}else{ -if("device-height"===_41.substring(l-13,l)){ -_47=screen.height; -if(_42!==null){ -if(_43==="length"){ -return ((min&&_47>=_44)||(max&&_47<_44)||(!min&&!max&&_47===_44)); -}else{ -return false; -} -}else{ -return _47>0; -} -}else{ -if("width"===_41.substring(l-5,l)){ -_46=document.documentElement.clientWidth||document.body.clientWidth; -if(_42!==null){ -if(_43==="length"){ -return ((min&&_46>=_44)||(max&&_46<_44)||(!min&&!max&&_46===_44)); -}else{ -return false; -} -}else{ -return _46>0; -} -}else{ -if("height"===_41.substring(l-6,l)){ -_47=document.documentElement.clientHeight||document.body.clientHeight; -if(_42!==null){ -if(_43==="length"){ -return ((min&&_47>=_44)||(max&&_47<_44)||(!min&&!max&&_47===_44)); -}else{ -return false; -} -}else{ -return _47>0; -} -}else{ -if("device-aspect-ratio"===_41.substring(l-19,l)){ -return _43==="aspect-ratio"&&screen.width*_44[1]===screen.height*_44[0]; -}else{ -if("color-index"===_41.substring(l-11,l)){ -var _48=Math.pow(2,screen.colorDepth); -if(_42!==null){ -if(_43==="absolute"){ -return ((min&&_48>=_44)||(max&&_48<_44)||(!min&&!max&&_48===_44)); -}else{ -return false; -} -}else{ -return _48>0; -} -}else{ -if("color"===_41.substring(l-5,l)){ -var _49=screen.colorDepth; -if(_42!==null){ -if(_43==="absolute"){ -return ((min&&_49>=_44)||(max&&_49<_44)||(!min&&!max&&_49===_44)); -}else{ -return false; -} -}else{ -return _49>0; -} -}else{ -if("resolution"===_41.substring(l-10,l)){ -var res; -if(_45==="dpcm"){ -res=_3d("1cm"); -}else{ -res=_3d("1in"); -} -if(_42!==null){ -if(_43==="resolution"){ -return ((min&&res>=_44)||(max&&res<_44)||(!min&&!max&&res===_44)); -}else{ -return false; -} -}else{ -return res>0; -} -}else{ -return false; -} -} -} -} -} -} -} -} -}; -var _4a=function(mq){ -var _4b=mq.getValid(); -var _4c=mq.getExpressions(); -var l=_4c.length; -if(l>0){ -for(var i=0;i0){ -s[c++]=","; -} -s[c++]=n; -} -} -if(s.length>0){ -_39[_39.length]=cssHelper.addStyle("@media "+s.join("")+"{"+mql.getCssText()+"}",false); -} -}; -var _4e=function(_4f){ -for(var i=0;i<_4f.length;i++){ -_4d(_4f[i]); -} -if(ua.ie){ -document.documentElement.style.display="block"; -setTimeout(function(){ -document.documentElement.style.display=""; -},0); -setTimeout(function(){ -cssHelper.broadcast("cssMediaQueriesTested"); -},100); -}else{ -cssHelper.broadcast("cssMediaQueriesTested"); -} -}; -var _50=function(){ -for(var i=0;i<_39.length;i++){ -cssHelper.removeStyle(_39[i]); -} -_39=[]; -cssHelper.mediaQueryLists(_4e); -}; -var _51=0; -var _52=function(){ -var _53=cssHelper.getViewportWidth(); -var _54=cssHelper.getViewportHeight(); -if(ua.ie){ -var el=document.createElement("div"); -el.style.position="absolute"; -el.style.top="-9999em"; -el.style.overflow="scroll"; -document.body.appendChild(el); -_51=el.offsetWidth-el.clientWidth; -document.body.removeChild(el); -} -var _55; -var _56=function(){ -var vpw=cssHelper.getViewportWidth(); -var vph=cssHelper.getViewportHeight(); -if(Math.abs(vpw-_53)>_51||Math.abs(vph-_54)>_51){ -_53=vpw; -_54=vph; -clearTimeout(_55); -_55=setTimeout(function(){ -if(!_3a()){ -_50(); -}else{ -cssHelper.broadcast("cssMediaQueriesTested"); -} -},500); -} -}; -window.onresize=function(){ -var x=window.onresize||function(){ -}; -return function(){ -x(); -_56(); -}; -}(); -}; -var _57=document.documentElement; -_57.style.marginLeft="-32767px"; -setTimeout(function(){ -_57.style.marginTop=""; -},20000); -return function(){ -if(!_3a()){ -cssHelper.addListener("newStyleParsed",function(el){ -_4e(el.cssHelperParsed.mediaQueryLists); -}); -cssHelper.addListener("cssMediaQueriesTested",function(){ -if(ua.ie){ -_57.style.width="1px"; -} -setTimeout(function(){ -_57.style.width=""; -_57.style.marginLeft=""; -},0); -cssHelper.removeListener("cssMediaQueriesTested",arguments.callee); -}); -_3c(); -_50(); -}else{ -_57.style.marginLeft=""; -} -_52(); -}; -}()); -try{ -document.execCommand("BackgroundImageCache",false,true); -} -catch(e){ -} - 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 index 0e88a5f4..83433adf 100644 Binary files a/logo.png and b/logo.png differ diff --git a/params.json b/params.json deleted file mode 100644 index 0fdc3636..00000000 --- a/params.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"Redmine-net-api","tagline":"redmine-net-api","body":"# redmine-net-api\r\n\r\nredmine-net-api is a library for communicating with a Redmine project management application\r\n\r\n* Uses [Redmine's REST API.](http://www.redmine.org/projects/redmine/wiki/Rest_api/)\r\n* Supports both XML and JSON(requires .NET Framework 3.5 or higher) formats\r\n* Supports GZipped responses from servers\r\n* This API provides access and basic CRUD operations (create, update, delete) for the resources described below\r\n * Attachments\r\n * Custom Fields\r\n * Enumerations \r\n * Groups\r\n * Issues \r\n * Issue Categories\r\n * Issue Relations\r\n * Issue Statuses\r\n * News(implementation for index only)\r\n * Projects\r\n * Project Memberships\r\n * Queries \r\n * Roles\r\n * Time Entries\r\n * Trackers\r\n * Users\r\n * Versions\r\n * Wiki Pages\r\n\r\n**Authentication**\r\n\r\nMost 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:\r\nusing your regular login/password via HTTP Basic authentication.\r\nusing 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:\r\n * passed in as a \"key\" parameter\r\n * passed in as a username with a random password via HTTP Basic authentication\r\n * passed in as a \"X-Redmine-API-Key\" HTTP header (added in Redmine 1.1.0)\r\nYou can find your API key on your account page ( /my/account ) when logged in, on the right-hand pane of the default layout.\r\n\r\n**User Impersonation**\r\n\r\nAs of Redmine 2.2.0, you can impersonate user through the REST API by setting the X-Redmine-Switch-User header of your API request. It must be set to a user login (eg. X-Redmine-Switch-User: 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.\r\n\r\nIf the login specified with the X-Redmine-Switch-User header does not exist or is not active, you will receive a 412 error response.\r\n\r\n\r\n
\r\n## Help me help you ##\r\nYour feedback is crucial. If you find anything that could improve the API, let me know. I will gladly receive your input and make the proper adjustments.\r\n\r\n**If you find this API useful let others know about it and/or rate it.**
\r\nYour contribution is always welcome.\r\n\r\n
\r\n## Licence ##\r\nThe 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.\r\n\r\n\r\n","google":"","note":"Don't delete this file! It's used internally to help with page regeneration."} \ No newline at end of file diff --git a/redmine-net-api.sln b/redmine-net-api.sln index 4b9268d1..6e9f665f 100644 --- a/redmine-net-api.sln +++ b/redmine-net-api.sln @@ -1,130 +1,101 @@ -ο»Ώ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.23107.0 +# 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}") = "UnitTestRedmineNetApi", "UnitTestRedmineNetApi\UnitTestRedmineNetApi.csproj", "{0DE30921-1273-41F6-91D6-AB17EC61E8F0}" +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("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AppVeyor", "AppVeyor", "{F20AEA6C-B957-4A83-9616-B91548B4C561}" + ProjectSection(SolutionItems) = preProject + appveyor.yml = appveyor.yml + EndProjectSection +EndProject +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("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docker", "Docker", "{1D340EEB-C535-45D4-80D7-ADD4434D7B77}" + ProjectSection(SolutionItems) = preProject + docker-compose.yml = docker-compose.yml + EndProjectSection +EndProject +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 Release|Any CPU = Release|Any CPU - Release|Mixed Platforms = Release|Mixed Platforms - Release|x86 = Release|x86 - RUNNING_ON_35_OR_ABOVE|Any CPU = RUNNING_ON_35_OR_ABOVE|Any CPU - RUNNING_ON_35_OR_ABOVE|Mixed Platforms = RUNNING_ON_35_OR_ABOVE|Mixed Platforms - RUNNING_ON_35_OR_ABOVE|x86 = RUNNING_ON_35_OR_ABOVE|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}.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 - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.RUNNING_ON_35_OR_ABOVE|Any CPU.ActiveCfg = Release|Any CPU - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.RUNNING_ON_35_OR_ABOVE|Any CPU.Build.0 = Release|Any CPU - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.RUNNING_ON_35_OR_ABOVE|Mixed Platforms.ActiveCfg = Release|Any CPU - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.RUNNING_ON_35_OR_ABOVE|Mixed Platforms.Build.0 = Release|Any CPU - {DA3E3C1B-2C01-4FB5-968B-3769BBF382BD}.RUNNING_ON_35_OR_ABOVE|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}.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 - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.RUNNING_ON_35_OR_ABOVE|Any CPU.ActiveCfg = Release|Any CPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.RUNNING_ON_35_OR_ABOVE|Any CPU.Build.0 = Release|Any CPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.RUNNING_ON_35_OR_ABOVE|Mixed Platforms.ActiveCfg = Release|Any CPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.RUNNING_ON_35_OR_ABOVE|Mixed Platforms.Build.0 = Release|Any CPU - {0D9B763C-A16B-463B-BDDD-0A0467DCD32E}.RUNNING_ON_35_OR_ABOVE|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}.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 - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.RUNNING_ON_35_OR_ABOVE|Any CPU.ActiveCfg = Release|Any CPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.RUNNING_ON_35_OR_ABOVE|Any CPU.Build.0 = Release|Any CPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.RUNNING_ON_35_OR_ABOVE|Mixed Platforms.ActiveCfg = Release|Any CPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.RUNNING_ON_35_OR_ABOVE|Mixed Platforms.Build.0 = Release|Any CPU - {89433E6E-F3D4-4B66-AC9A-1B7F4345BBA4}.RUNNING_ON_35_OR_ABOVE|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}.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 - {82796546-0F57-425B-BB77-751FA24D49D5}.RUNNING_ON_35_OR_ABOVE|Any CPU.ActiveCfg = Release|Any CPU - {82796546-0F57-425B-BB77-751FA24D49D5}.RUNNING_ON_35_OR_ABOVE|Any CPU.Build.0 = Release|Any CPU - {82796546-0F57-425B-BB77-751FA24D49D5}.RUNNING_ON_35_OR_ABOVE|Mixed Platforms.ActiveCfg = Release|Any CPU - {82796546-0F57-425B-BB77-751FA24D49D5}.RUNNING_ON_35_OR_ABOVE|Mixed Platforms.Build.0 = Release|Any CPU - {82796546-0F57-425B-BB77-751FA24D49D5}.RUNNING_ON_35_OR_ABOVE|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}.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 - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.RUNNING_ON_35_OR_ABOVE|Any CPU.ActiveCfg = Release|Any CPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.RUNNING_ON_35_OR_ABOVE|Any CPU.Build.0 = Release|Any CPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.RUNNING_ON_35_OR_ABOVE|Mixed Platforms.ActiveCfg = Release|Any CPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.RUNNING_ON_35_OR_ABOVE|Mixed Platforms.Build.0 = Release|Any CPU - {1E80FE6C-6607-42BD-B6E3-4FE68DBA8E5E}.RUNNING_ON_35_OR_ABOVE|x86.ActiveCfg = Release|Any CPU - {0DE30921-1273-41F6-91D6-AB17EC61E8F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0DE30921-1273-41F6-91D6-AB17EC61E8F0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0DE30921-1273-41F6-91D6-AB17EC61E8F0}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {0DE30921-1273-41F6-91D6-AB17EC61E8F0}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {0DE30921-1273-41F6-91D6-AB17EC61E8F0}.Debug|x86.ActiveCfg = Debug|Any CPU - {0DE30921-1273-41F6-91D6-AB17EC61E8F0}.Debug|x86.Build.0 = Debug|Any CPU - {0DE30921-1273-41F6-91D6-AB17EC61E8F0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0DE30921-1273-41F6-91D6-AB17EC61E8F0}.Release|Any CPU.Build.0 = Release|Any CPU - {0DE30921-1273-41F6-91D6-AB17EC61E8F0}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {0DE30921-1273-41F6-91D6-AB17EC61E8F0}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {0DE30921-1273-41F6-91D6-AB17EC61E8F0}.Release|x86.ActiveCfg = Release|Any CPU - {0DE30921-1273-41F6-91D6-AB17EC61E8F0}.Release|x86.Build.0 = Release|Any CPU - {0DE30921-1273-41F6-91D6-AB17EC61E8F0}.RUNNING_ON_35_OR_ABOVE|Any CPU.ActiveCfg = Release|Any CPU - {0DE30921-1273-41F6-91D6-AB17EC61E8F0}.RUNNING_ON_35_OR_ABOVE|Any CPU.Build.0 = Release|Any CPU - {0DE30921-1273-41F6-91D6-AB17EC61E8F0}.RUNNING_ON_35_OR_ABOVE|Mixed Platforms.ActiveCfg = Release|Any CPU - {0DE30921-1273-41F6-91D6-AB17EC61E8F0}.RUNNING_ON_35_OR_ABOVE|Mixed Platforms.Build.0 = Release|Any CPU - {0DE30921-1273-41F6-91D6-AB17EC61E8F0}.RUNNING_ON_35_OR_ABOVE|x86.ActiveCfg = Release|Any CPU - {0DE30921-1273-41F6-91D6-AB17EC61E8F0}.RUNNING_ON_35_OR_ABOVE|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-net40-api-signed/redmine-net-api.snk b/redmine-net-api.snk 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/ExtensionMethods.cs b/redmine-net20-api/ExtensionMethods.cs deleted file mode 100644 index 7aaf4a26..00000000 --- a/redmine-net20-api/ExtensionMethods.cs +++ /dev/null @@ -1,286 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 Adrian Popescu, Dorin Huzum., 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; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Xml; -using System.Xml.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api -{ - public static class ExtensionMethods - { - /// - /// Reads the attribute as int. - /// - /// The reader. - /// Name of the attribute. - /// - public static int ReadAttributeAsInt(this XmlReader reader, string attributeName) - { - try - { - var attribute = reader.GetAttribute(attributeName); - int result; - - if (String.IsNullOrEmpty(attribute) || !Int32.TryParse(attribute, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out result)) return default(int); - - return result; - } - catch - { - return -1; - } - } - - public static int? ReadAttributeAsNullableInt(this XmlReader reader, string attributeName) - { - try - { - var attribute = reader.GetAttribute(attributeName); - int result; - - if (String.IsNullOrEmpty(attribute) || !Int32.TryParse(attribute, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out result)) return default(int?); - - return result; - } - catch - { - return null; - } - } - - /// - /// Reads the attribute as boolean. - /// - /// The reader. - /// Name of the attribute. - /// - public static bool ReadAttributeAsBoolean(this XmlReader reader, string attributeName) - { - try - { - var attribute = reader.GetAttribute(attributeName); - bool result; - - if (String.IsNullOrEmpty(attribute) || !Boolean.TryParse(attribute, out result)) return false; - - return result; - } - catch - { - return false; - } - } - - /// - /// 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)) - { - 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; - } - - 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; - } - - /// - /// 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)); - } - - public static void WriteIdIfNotNull(this Dictionary dictionary, IdentifiableName ident, String key) - { - if (ident != null) dictionary.Add(key, ident.Id); - } - - /// - /// Writes string empty if T has default value or null. - /// - /// - /// The writer. - /// The value. - /// The tag. - public static void WriteValue(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)); - } - - public static void WriteDate(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))); - } - - public static void WriteArray(this XmlWriter writer, IEnumerable col, string elementName) - { - writer.WriteStartElement(elementName); - writer.WriteAttributeString("type", "array"); - if (col != null) - { - foreach (var item in col) - { - new XmlSerializer(item.GetType()).Serialize(writer, item); - } - } - writer.WriteEndElement(); - } - - public static void WriteIfNotDefaultOrNull(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); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/RedmineException.cs b/redmine-net20-api/RedmineException.cs deleted file mode 100644 index 8dcaa8bd..00000000 --- a/redmine-net20-api/RedmineException.cs +++ /dev/null @@ -1,42 +0,0 @@ -/* - Copyright 2011 - 2015 Adrian Popescu, Dorin Huzum., 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.Runtime.Serialization; - -namespace Redmine.Net.Api -{ - public class RedmineException : Exception - { - public RedmineException() - : base() { } - - public RedmineException(string message) - : base(message) { } - - public RedmineException(string format, params object[] args) - : base(string.Format(format, args)) { } - - public RedmineException(string message, Exception innerException) - : base(message, innerException) { } - - public RedmineException(string format, Exception innerException, params object[] args) - : base(string.Format(format, args), innerException) { } - - protected RedmineException(SerializationInfo info, StreamingContext context) - : base(info, context) { } - } -} \ 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 89fae756..00000000 --- a/redmine-net20-api/RedmineManager.cs +++ /dev/null @@ -1,818 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 Adrian Popescu, Dorin Huzum., 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.Collections.Specialized; -using System.Diagnostics; -using System.Globalization; -using System.IO; - -using System.Net; -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Text.RegularExpressions; -using System.Xml; -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 partial class RedmineManager - { - private const string REQUEST_FORMAT = "{0}/{1}/{2}.xml"; - private const string FORMAT = "{0}/{1}.xml"; - - private const string WIKI_INDEX_FORMAT = "{0}/projects/{1}/wiki/index.xml"; - private const string WIKI_PAGE_FORMAT = "{0}/projects/{1}/wiki/{2}.xml"; - private const string WIKI_VERSION_FORMAT = "{0}/projects/{1}/wiki/{2}/{3}.xml"; - - private const string ENTITY_WITH_PARENT_FORMAT = "{0}/{1}/{2}/{3}.xml"; - - private const string CURRENT_USER_URI = "current"; - private const string PUT = "PUT"; - private const string POST = "POST"; - private const string DELETE = "DELETE"; - - private readonly Dictionary urls = 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 host, apiKey, basicAuthorization; - - private readonly CredentialCache cache; - - /// - /// 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. - /// - 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. - /// - public string ImpersonateUser { get; set; } - - /// - /// Initializes a new instance of the class. - /// - /// The host. - /// - /// if set to true [verify server cert]. - public RedmineManager(string host, bool verifyServerCert = true) - { - PageSize = 25; - - 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!"); - - this.host = host; - - 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]. - public RedmineManager(string host, string apiKey, bool verifyServerCert = true) - : this(host, verifyServerCert) - { - PageSize = 25; - this.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]. - public RedmineManager(string host, string login, string password, bool verifyServerCert = true) - : this(host, verifyServerCert) - { - PageSize = 25; - 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!"); - - cache = new CredentialCache { { uriResult, "Basic", new NetworkCredential(login, password) } }; - basicAuthorization = "Basic " + Convert.ToBase64String(Encoding.ASCII.GetBytes(login + ":" + password)); - } - - /// - /// 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) - { - return ExecuteDownload(string.Format(REQUEST_FORMAT, host, urls[typeof(User)], CURRENT_USER_URI), "GetCurrentUser", parameters); - } - - /// - /// Returns a list of users. - /// - /// get only users with the given status. Default is 1 (active users) - /// filter users on their login, firstname, lastname and mail ; if the pattern contains a space, it will also return users whose firstname match the first word or lastname match the second word. - /// get only users who are members of the given group - /// - public IList GetUsers(UserStatus userStatus = UserStatus.STATUS_ACTIVE, string name = null, int groupId = 0) - { - var filters = new NameValueCollection { { "status", ((int)userStatus).ToString(CultureInfo.InvariantCulture) } }; - - if (!string.IsNullOrEmpty(name)) filters.Add("name", name); - - if (groupId > 0) filters.Add("groupId", groupId.ToString(CultureInfo.InvariantCulture)); - - return GetTotalObjectList(filters); - } - - public void AddWatcher(int issueId, int userId) - { - ExecuteUpload(string.Format(REQUEST_FORMAT, host, urls[typeof(Issue)], issueId + "/watchers"), POST, "" + userId + "", "AddWatcher"); - } - - public void RemoveWatcher(int issueId, int userId) - { - ExecuteUpload(string.Format(REQUEST_FORMAT, host, urls[typeof(Issue)], issueId + "/watchers/" + userId), DELETE, string.Empty, "RemoveWatcher"); - } - - /// - /// Adds an existing user to a group. - /// - /// The group id. - /// The user id. - public void AddUser(int groupId, int userId) - { - ExecuteUpload(string.Format(REQUEST_FORMAT, host, urls[typeof(Group)], groupId + "/users"), POST, "" + userId + "", "AddUser"); - } - - /// - /// Removes an user from a group. - /// - /// The group id. - /// The user id. - public void DeleteUser(int groupId, int userId) - { - ExecuteUpload(string.Format(REQUEST_FORMAT, host, urls[typeof(Group)], groupId + "/users/" + userId), DELETE, string.Empty, "DeleteUser"); - } - - /// - /// Downloads the user whose credentials are used to access the API. This method does not block the calling thread. - /// - /// Returns the Guid associated with the async request. - /// An error occurred during deserialization. The original exception is available - /// using the System.Exception.InnerException property. - /// - /// Returns the details of a wiki page or the details of an old version of a wiki page if the version parameter is set. - /// - /// The project id or identifier. - /// - /// attachments - /// The accepted parameters are: memberships and groups (added in 2.1). - /// - /// The wiki page name. - /// The version of the wiki page. - /// - public WikiPage GetWikiPage(string projectId, NameValueCollection parameters, string pageName, uint version = 0) - { - string address = version == 0 - ? string.Format(WIKI_PAGE_FORMAT, host, projectId, pageName) - : string.Format(WIKI_VERSION_FORMAT, host, projectId, pageName, version); - - return ExecuteDownload(address, "GetWikiPage", parameters); - } - - /// - /// Returns the list of all pages in a project wiki. - /// - /// The project id or identifier. - /// - public IList GetAllWikiPages(string projectId) - { - int totalCount; - return ExecuteDownloadList(string.Format(WIKI_INDEX_FORMAT, host, projectId), "GetAllWikiPages", "wiki", out totalCount); - } - - /// - /// 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) - { - string result = Serialize(wikiPage); - - if (string.IsNullOrEmpty(result)) return null; - - return ExecuteUpload(string.Format(WIKI_PAGE_FORMAT, host, projectId, pageName), PUT, result, "CreateOrUpdateWikiPage"); - } - - /// - /// 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) - { - ExecuteUpload(string.Format(WIKI_PAGE_FORMAT, host, projectId, pageName), DELETE, string.Empty, "DeleteWikiPage"); - } - - /// - /// 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) - { - using (var wc = CreateUploadWebClient()) - { - try - { - var response = wc.UploadData(string.Format(FORMAT, host, "uploads"), data); - var responseString = Encoding.ASCII.GetString(response); - return Deserialize(responseString); - } - catch (WebException webException) - { - HandleWebException(webException, "Upload"); - } - } - - return null; - } - - public byte[] DownloadFile(string address) - { - using (var wc = CreateUploadWebClient()) - { - try - { - return wc.DownloadData(address); - } - catch (WebException webException) - { - HandleWebException(webException, "Download"); - } - } - - return null; - } - - /// - /// Returns a paginated list of objects. - /// - /// The type of objects to retrieve. - /// Optional filters and/or optional fetched data. - /// Returns a paginated list of objects. - /// By default only 25 results can be retrieved by 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. - /// - public IList GetObjectList(NameValueCollection parameters) where T : class, new() - { - int totalCount; - return GetObjectList(parameters, out totalCount); - } - - /// - /// Returns a paginated list of objects. - /// - /// The type of objects to retrieve. - /// Optional filters and/or optional fetched data. - /// Provide information about the total object count available in Redmine. - /// Returns a paginated list of objects. - /// By default only 25 results can be retrieved by 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. - /// - /// - public IList GetObjectList(NameValueCollection parameters, out int totalCount) where T : class, new() - { - totalCount = -1; - if (!urls.ContainsKey(typeof(T))) return null; - - var type = typeof(T); - string address; - if (type == typeof(Version) || type == typeof(IssueCategory) || type == typeof(ProjectMembership)) - { - var projectId = GetOwnerId(parameters, "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."); - - address = string.Format(ENTITY_WITH_PARENT_FORMAT, host, "projects", projectId, urls[type]); - } - else - if (type == typeof(IssueRelation)) - { - string issueId = GetOwnerId(parameters, "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"); - - address = string.Format(ENTITY_WITH_PARENT_FORMAT, host, "issues", issueId, urls[type]); - } - else - address = string.Format(FORMAT, host, urls[type]); - - return ExecuteDownloadList(address, "GetObjectList<" + type.Name + ">", urls[type], out totalCount, parameters); - } - - /// - /// 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. - /// 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. - /// - public IList GetTotalObjectList(NameValueCollection parameters) where T : class, new() - { - int totalCount, pageSize; - List resultList = null; - if (parameters == null) parameters = new NameValueCollection(); - int offset = 0; - int.TryParse(parameters["limit"], out pageSize); - if (pageSize == default(int)) - { - pageSize = PageSize > 0 ? PageSize : 25; - parameters.Set("limit", pageSize.ToString(CultureInfo.InvariantCulture)); - } - do - { - parameters.Set("offset", offset.ToString(CultureInfo.InvariantCulture)); - var tempResult = (List)GetObjectList(parameters, out totalCount); - if (resultList == null) - resultList = tempResult; - else - resultList.AddRange(tempResult); - offset += pageSize; - } - while (offset < totalCount); - return resultList; - } - - /// - /// Returns a Redmine object. - /// - /// 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 type = typeof(T); - - return !urls.ContainsKey(type) ? null : ExecuteDownload(string.Format(REQUEST_FORMAT, host, urls[type], id), "GetObject<" + type.Name + ">", parameters); - } - - /// - /// 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 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. - /// - /// - /// - /// 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 type = typeof(T); - - if (!urls.ContainsKey(type)) return null; - - var result = Serialize(obj); - - if (string.IsNullOrEmpty(result)) return null; - - string address; - - if (type == typeof(Version) || type == typeof(IssueCategory) || type == typeof(ProjectMembership)) - { - if (string.IsNullOrEmpty(ownerId)) throw new RedmineException("The owner id(project id) is mandatory!"); - address = string.Format(ENTITY_WITH_PARENT_FORMAT, host, "projects", ownerId, urls[type]); - } - else - if (type == typeof(IssueRelation)) - { - if (string.IsNullOrEmpty(ownerId)) throw new RedmineException("The owner id(issue id) is mandatory!"); - address = string.Format(ENTITY_WITH_PARENT_FORMAT, host, "issues", ownerId, urls[type]); - } - else - address = string.Format(FORMAT, host, urls[type]); - - return ExecuteUpload(address, POST, result, "CreateObject<" + type.Name + ">"); - } - - /// - /// 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 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. - /// - /// When trying to update an object with invalid or missing attribute parameters, you will get a 422 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 type = typeof(T); - - if (!urls.ContainsKey(type)) return; - - var request = Serialize(obj); - if (string.IsNullOrEmpty(request)) return; - - request = Regex.Replace(request, @"\r\n|\r|\n", "\r\n"); - - string address; - - if (type == typeof(IssueCategory) || type == typeof(ProjectMembership)) - { - if (string.IsNullOrEmpty(projectId)) throw new RedmineException("The project owner id is mandatory!"); - address = string.Format(ENTITY_WITH_PARENT_FORMAT, host, "projects", projectId, urls[type]); - } - else - { - address = string.Format(REQUEST_FORMAT, host, urls[type], id); - } - - ExecuteUpload(address, PUT, request, "UpdateObject<" + type.Name + ">"); - } - - /// - /// Deletes the Redmine object. - /// - /// The type of objects to delete. - /// The id of the object to delete - /// Optional filters and/or optional fetched data. - /// - /// - public void DeleteObject(string id, NameValueCollection parameters) where T : class - { - var type = typeof(T); - - if (!urls.ContainsKey(typeof(T))) return; - - ExecuteUpload(string.Format(REQUEST_FORMAT, host, urls[type], id), DELETE, string.Empty, "DeleteObject<" + type.Name + ">"); - } - - /// - /// Creates the Redmine web client. - /// - /// The parameters. - /// - /// - protected WebClient CreateWebClient(NameValueCollection parameters) - { - var webClient = new RedmineWebClient(); - - if (parameters != null) webClient.QueryString = parameters; - - if (!string.IsNullOrEmpty(apiKey)) - { - webClient.QueryString["key"] = apiKey; - } - else - { - if (cache != null) webClient.Credentials = cache; - } - - if (!string.IsNullOrEmpty(ImpersonateUser)) webClient.Headers.Add("X-Redmine-Switch-User", ImpersonateUser); - - webClient.Headers.Add("Content-Type", "application/xml; charset=utf-8"); - webClient.Encoding = Encoding.UTF8; - return webClient; - } - - /// - /// Creates the Redmine web client. - /// - /// The parameters. - /// - /// - protected WebClient CreateUploadWebClient(NameValueCollection parameters = null) - { - var webClient = new RedmineWebClient(); - - if (parameters != null) webClient.QueryString = parameters; - - if (!string.IsNullOrEmpty(apiKey)) - { - webClient.QueryString["key"] = apiKey; - } - else - { - if (cache != null) webClient.Credentials = cache; - } - - webClient.UseDefaultCredentials = false; - - webClient.Headers.Add("Content-Type", "application/octet-stream"); - // Workaround - it seems that WebClient doesn't send credentials in each POST request - webClient.Headers.Add("Authorization", basicAuthorization); - - return webClient; - } - - /// - /// This is to take care of SSL certification validation which are not issued by Trusted Root CA. Recommended for testing only. - /// - /// The sender. - /// The cert. - /// The chain. - /// The error. - /// - /// - protected bool RemoteCertValidate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors error) - { - //Cert Validation Logic - return true; - } - - private void HandleWebException(WebException exception, string method) - { - if (exception == null) return; - - switch (exception.Status) - { - case WebExceptionStatus.Timeout: throw new RedmineException("Timeout!"); - case WebExceptionStatus.NameResolutionFailure: throw new RedmineException("Bad domain name!"); - case WebExceptionStatus.ProtocolError: - { - var response = (HttpWebResponse)exception.Response; - switch ((int)response.StatusCode) - { - case (int)HttpStatusCode.InternalServerError: - case (int)HttpStatusCode.Unauthorized: - case (int)HttpStatusCode.NotFound: - case (int)HttpStatusCode.Forbidden: - throw new RedmineException(response.StatusDescription); - - case (int)HttpStatusCode.Conflict: - throw new RedmineException("The page that you are trying to update is staled!"); - - case 422: - var errors = ReadWebExceptionResponse(exception.Response); - string message = string.Empty; - if (errors != null) - { - foreach (var error in errors) - { - message += error.Info + Environment.NewLine; - } - } - throw new RedmineException(method + " has invalid or missing attribute parameters: " + message); - - case (int)HttpStatusCode.NotAcceptable: throw new RedmineException(response.StatusDescription); - } - } - break; - - default: throw new RedmineException(exception.Message); - } - } - - private static string GetOwnerId(NameValueCollection parameters, string parameterName) - { - if (parameters == null) return null; - string ownerId = parameters.Get(parameterName); - return string.IsNullOrEmpty(ownerId) ? null : ownerId; - } - - private IEnumerable ReadWebExceptionResponse(WebResponse webResponse) - { - using (var dataStream = webResponse.GetResponseStream()) - { - if (dataStream == null) return null; - var reader = new StreamReader(dataStream); - - var responseFromServer = reader.ReadToEnd(); - - if (responseFromServer.Trim().Length > 0) - { - try - { - int totalCount; - return DeserializeList(responseFromServer, "errors", out totalCount); - } - catch (Exception ex) - { - Trace.TraceError(ex.Message); - } - } - return null; - } - } - - private string Serialize(T obj) where T : class, new() - { - return RedmineSerialization.ToXML(obj); - } - - private T Deserialize(string response) where T : class, new() - { - return RedmineSerialization.FromXML(response); - } - - private IList DeserializeList(string response, string jsonRoot, out int totalCount) where T : class, new() - { - using (var text = new StringReader(response)) - { - using (var xmlReader = new XmlTextReader(text)) - { - xmlReader.WhitespaceHandling = WhitespaceHandling.None; - xmlReader.Read(); - xmlReader.Read(); - - totalCount = xmlReader.ReadAttributeAsInt("total_count"); - - return xmlReader.ReadElementContentAsCollection(); - } - } - } - - private void ExecuteUpload(string address, string actionType, string data, string methodName) - { - using (var wc = CreateWebClient(null)) - { - try - { - if (actionType == POST || actionType == DELETE || actionType == PUT) - { - wc.UploadString(address, actionType, data); - } - } - catch (WebException webException) - { - HandleWebException(webException, methodName); - } - } - } - - private T ExecuteUpload(string address, string actionType, string data, string methodName) where T : class , new() - { - using (var wc = CreateWebClient(null)) - { - try - { - if (actionType == POST || actionType == DELETE || actionType == PUT) - { - var response = wc.UploadString(address, actionType, data); - - return Deserialize(response); - } - } - catch (WebException webException) - { - HandleWebException(webException, methodName); - } - return default(T); - } - } - - private T ExecuteDownload(string address, string methodName, NameValueCollection parameters = null) where T : class, new() - { - using (var wc = CreateWebClient(parameters)) - { - try - { - var response = wc.DownloadString(address); - if (!string.IsNullOrEmpty(response)) - return Deserialize(response); - } - catch (WebException webException) - { - HandleWebException(webException, methodName); - } - return default(T); - } - } - - private IList ExecuteDownloadList(string address, string methodName, string jsonRoot, out int totalCount, NameValueCollection parameters = null) where T : class, new() - { - totalCount = -1; - using (var wc = CreateWebClient(parameters)) - { - try - { - var response = wc.DownloadString(address); - return DeserializeList(response, jsonRoot, out totalCount); - } - catch (WebException webException) - { - HandleWebException(webException, methodName); - } - return null; - } - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/RedmineSerialization.cs b/redmine-net20-api/RedmineSerialization.cs deleted file mode 100644 index 1b8c8417..00000000 --- a/redmine-net20-api/RedmineSerialization.cs +++ /dev/null @@ -1,81 +0,0 @@ -/* - Copyright 2011 - 2015 Adrian Popescu, Dorin Huzum., 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.IO; -using System.Xml; -using System.Xml.Serialization; - -namespace Redmine.Net.Api -{ - public static partial class RedmineSerialization - { - /// - /// 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. - /// - public 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. - public 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. - public static object FromXML(string xml, Type type) - { - using (var text = new StringReader(xml)) - { - var sr = new XmlSerializer(type); - return sr.Deserialize(text); - } - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/RedmineWebClient.cs b/redmine-net20-api/RedmineWebClient.cs deleted file mode 100644 index 24c2c5bc..00000000 --- a/redmine-net20-api/RedmineWebClient.cs +++ /dev/null @@ -1,49 +0,0 @@ -/* - Copyright 2011 - 2015 Adrian Popescu, Dorin Huzum., 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.Net; - -namespace Redmine.Net.Api -{ - /// - /// - /// - public class RedmineWebClient :WebClient - { - private readonly CookieContainer container = new CookieContainer(); - - protected override WebRequest GetWebRequest(Uri address) - { - Headers.Add(HttpRequestHeader.Cookie, "redmineCookie"); - - var wr = base.GetWebRequest(address); - var httpWebRequest = wr as HttpWebRequest; - - if (httpWebRequest != null) - { - httpWebRequest.CookieContainer = container; - - httpWebRequest.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; - - 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 100644 index 068e5a7c..00000000 --- a/redmine-net20-api/Types/Attachment.cs +++ /dev/null @@ -1,123 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 1.3 - /// - [XmlRoot("attachment")] - public class Attachment : Identifiable, IXmlSerializable, IEquatable - { - /// - /// Gets or sets the name of the file. - /// - /// The name of the file. - [XmlElement("filename")] - public String FileName { get; set; } - - /// - /// Gets or sets the size of the file. - /// - /// The size of the file. - [XmlElement("filesize")] - public int FileSize { get; set; } - - /// - /// Gets or sets the type of the content. - /// - /// The type of the content. - [XmlElement("content_type")] - public String ContentType { get; set; } - - /// - /// Gets or sets the description. - /// - /// The description. - [XmlElement("description")] - public String Description { get; set; } - - /// - /// Gets or sets the content URL. - /// - /// The content URL. - [XmlElement("content_url")] - public String ContentUrl { get; set; } - - /// - /// Gets or sets the author. - /// - /// The author. - [XmlElement("author")] - public IdentifiableName Author { get; set; } - - /// - /// Gets or sets the created on. - /// - /// The created on. - [XmlElement("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 "id": Id = reader.ReadElementContentAsInt(); break; - - case "filename": FileName = reader.ReadElementContentAsString(); break; - - case "filesize": FileSize = reader.ReadElementContentAsInt(); break; - - case "content_type": ContentType = reader.ReadElementContentAsString(); break; - - case "author": Author = new IdentifiableName(reader); break; - - case "created_on": CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case "description": Description = reader.ReadElementContentAsString(); break; - - case "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 && Description == other.Description && ContentUrl == other.ContentUrl && Author == other.Author && CreatedOn == other.CreatedOn); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/ChangeSet.cs b/redmine-net20-api/Types/ChangeSet.cs deleted file mode 100644 index e899574f..00000000 --- a/redmine-net20-api/Types/ChangeSet.cs +++ /dev/null @@ -1,91 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; - -namespace Redmine.Net.Api.Types -{ - /// - /// - /// - [XmlRoot("changeset")] - public class ChangeSet : IXmlSerializable, IEquatable - { - /// - /// - /// - [XmlAttribute("revision")] - public int Revision { get; set; } - - /// - /// - /// - [XmlElement("user")] - public IdentifiableName User { get; set; } - - /// - /// - /// - [XmlElement("comments")] - public string Comments { get; set; } - - /// - /// - /// - [XmlElement("committed_on", IsNullable = true)] - public DateTime? CommittedOn { 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; - } - - Revision = reader.ReadAttributeAsInt("revision"); - - switch (reader.Name) - { - case "user": User = new IdentifiableName(reader); break; - - case "comments": Comments = reader.ReadElementContentAsString(); break; - - case "committed_on": CommittedOn = reader.ReadElementContentAsNullableDateTime(); break; - - default: reader.Read(); break; - } - } - } - - public void WriteXml(XmlWriter writer) { } - - public bool Equals(ChangeSet other) - { - if (other == null) return false; - - return Revision == other.Revision && User == other.User && Comments == other.Comments && CommittedOn == other.CommittedOn; - } - } -} \ 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 100644 index 5f5e13ee..00000000 --- a/redmine-net20-api/Types/CustomField.cs +++ /dev/null @@ -1,148 +0,0 @@ -/* - Copyright 2011 - 2015 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; - -namespace Redmine.Net.Api.Types -{ - [XmlRoot("custom_field")] - public class CustomField : IXmlSerializable, IEquatable - { - [XmlElement("id")] - public int Id { get; set; } - - [XmlElement("name")] - public string Name { get; set; } - - [XmlElement("customized_type")] - public string CustomizedType { get; set; } - - [XmlElement("field_format")] - public string FieldFormat { get; set; } - - [XmlElement("regexp")] - public string Regexp { get; set; } - - [XmlElement("min_length")] - public int? MinLength { get; set; } - - [XmlElement("max_length")] - public int? MaxLength { get; set; } - - [XmlElement("is_required")] - public bool IsRequired { get; set; } - - [XmlElement("is_filter")] - public bool IsFilter { get; set; } - - [XmlElement("searchable")] - public bool Searchable { get; set; } - - [XmlElement("multiple")] - public bool Multiple { get; set; } - - [XmlElement("default_value")] - public string DefaultValue { get; set; } - - [XmlElement("visible")] - public bool Visible { get; set; } - - [XmlArray("possible_values")] - [XmlArrayItem("possible_value")] - public IList PossibleValues { get; set; } - - [XmlArray("trackers")] - [XmlArrayItem("tracker")] - public IList Trackers { get; set; } - - [XmlArray("roles")] - [XmlArrayItem("role")] - public IList 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 "id": Id = reader.ReadElementContentAsInt(); break; - - case "name": Name = reader.ReadElementContentAsString(); break; - - case "customized_type": CustomizedType = reader.ReadElementContentAsString(); break; - - case "field_format": FieldFormat = reader.ReadElementContentAsString(); break; - - case "regexp": Regexp = reader.ReadElementContentAsString(); break; - - case "min_length": MinLength = reader.ReadElementContentAsNullableInt(); break; - - case "max_length": MaxLength = reader.ReadElementContentAsNullableInt(); break; - - case "is_required": IsRequired = reader.ReadElementContentAsBoolean(); break; - - case "is_filter": IsFilter = reader.ReadElementContentAsBoolean(); break; - - case "searchable": Searchable = reader.ReadElementContentAsBoolean(); break; - - case "visible": Visible = reader.ReadElementContentAsBoolean(); break; - - case "default_value": DefaultValue = reader.ReadElementContentAsString(); break; - - case "multiple": Multiple = reader.ReadElementContentAsBoolean(); break; - - case "trackers": - Trackers = reader.ReadElementContentAsCollection(); - break; - - case "roles": - Roles = reader.ReadElementContentAsCollection(); - break; - case "possible_values": PossibleValues = reader.ReadElementContentAsCollection(); break; - - default: reader.Read(); break; - } - } - } - - public void WriteXml(XmlWriter writer) { } - - public bool Equals(CustomField other) - { - if (other == null) return false; - return (Id == other.Id && Name == other.Name && Multiple == other.Multiple && IsFilter == other.IsFilter && IsRequired == other.IsRequired - && Searchable == other.Searchable && Visible == other.Visible - && CustomizedType == other.CustomizedType && DefaultValue == other.DefaultValue - && FieldFormat == other.FieldFormat && MaxLength == other.MaxLength && MinLength == other.MinLength && Regexp == other.Regexp - && Equals(Roles, other.Roles) - && Equals(Trackers, other.Trackers) - && Equals(PossibleValues, other.PossibleValues)); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/CustomFieldRole.cs b/redmine-net20-api/Types/CustomFieldRole.cs deleted file mode 100644 index 1c016292..00000000 --- a/redmine-net20-api/Types/CustomFieldRole.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Xml.Serialization; - -namespace Redmine.Net.Api.Types -{ - [XmlRoot("role")] - public class CustomFieldRole : IdentifiableName, IEquatable - { - #region Implementation of IEquatable - - public bool Equals(CustomFieldRole other) { if (other == null) return false; - return Id == other.Id && Name == other.Name; - } - - public override string ToString() - { - return Id + ", " + Name; - } - - #endregion - } -} \ 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 100644 index 9a84f0ae..00000000 --- a/redmine-net20-api/Types/Detail.cs +++ /dev/null @@ -1,102 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; - -namespace Redmine.Net.Api.Types -{ - /// - /// - /// - [XmlRoot("detail")] - public class Detail : IXmlSerializable, IEquatable - { - /// - /// Gets or sets the property. - /// - /// - /// The property. - /// - [XmlAttribute("property")] - public string Property { get; set; } - - /// - /// Gets or sets the status id. - /// - /// - /// The status id. - /// - [XmlAttribute("name")] - public string StatusId { get; set; } - - /// - /// Gets or sets the old value. - /// - /// - /// The old value. - /// - [XmlElement("old_value")] - public string OldValue { get; set; } - - /// - /// Gets or sets the new value. - /// - /// - /// The new value. - /// - [XmlElement("new_value")] - public string NewValue { get; set; } - - public XmlSchema GetSchema() { return null; } - - public void ReadXml(XmlReader reader) - { - Property = reader.GetAttribute("property"); - StatusId = reader.GetAttribute("name"); - - reader.Read(); - - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case "old_value": OldValue = reader.ReadElementContentAsString(); break; - - case "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 == other.Property && StatusId == other.StatusId && OldValue == other.OldValue && NewValue == other.NewValue; - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/Error.cs b/redmine-net20-api/Types/Error.cs deleted file mode 100644 index 33ccc756..00000000 --- a/redmine-net20-api/Types/Error.cs +++ /dev/null @@ -1,58 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; - -namespace Redmine.Net.Api.Types -{ - [XmlRoot("error")] - public class Error: IXmlSerializable - { - [XmlText] - public string Info { get; set; } - - public override string ToString() - { - return Info; - } - - 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 "error": Info = reader.ReadElementContentAsString(); break; - - default: reader.Read(); break; - } - } - } - - public void WriteXml(XmlWriter writer) { } - } -} \ 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 100644 index 19648ac5..00000000 --- a/redmine-net20-api/Types/Group.cs +++ /dev/null @@ -1,157 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 2.1 - /// - [XmlRoot("group")] - public class Group : IXmlSerializable, IEquatable - { - /// - /// Gets or sets the id. - /// - /// - /// The id. - /// - [XmlElement("id")] - public int Id { get; set; } - - /// - /// Gets or sets the name. - /// - /// - /// The name. - /// - [XmlElement("name")] - public string Name { get; set; } - - /// - /// Represents the group's users. - /// - [XmlArray("users")] - [XmlArrayItem("user")] - public List Users { get; set; } - - /// - /// Gets or sets the custom fields. - /// - /// The custom fields. - [XmlArray("custom_fields")] - [XmlArrayItem("custom_field")] - public IList CustomFields { get; set; } - - /// - /// Gets or sets the custom fields. - /// - /// The custom fields. - [XmlArray("memberships")] - [XmlArrayItem("membership")] - public IList Memberships { get; set; } - - #region Implementation of IXmlSerializable - - /// - /// This method is reserved and should not be used. When implementing the IXmlSerializable interface, you should return null (Nothing in Visual Basic) from this method, and instead, if specifying a custom schema is required, apply the to the class. - /// - /// - /// An that describes the XML representation of the object that is produced by the method and consumed by the method. - /// - public XmlSchema GetSchema() { return null; } - - /// - /// Generates an object from its XML representation. - /// - /// The stream from which the object is deserialized. - public void ReadXml(XmlReader reader) - { - reader.Read(); - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case "id": Id = reader.ReadElementContentAsInt(); break; - - case "name": Name = reader.ReadElementContentAsString(); break; - - case "users": Users = reader.ReadElementContentAsCollection(); break; - - case "custom_fields": CustomFields = reader.ReadElementContentAsCollection(); break; - - case "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 void WriteXml(XmlWriter writer) - { - writer.WriteElementString("name", Name); - - if (Users == null) return; - - writer.WriteStartElement("user_ids"); - writer.WriteAttributeString("type", "array"); - foreach (var userId in Users) - { - new XmlSerializer(typeof(int)).Serialize(writer, userId.Id); - } - writer.WriteEndElement(); - } - - #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 == other.Users && CustomFields == other.CustomFields && Memberships == other.Memberships; - } - - #endregion - - public override string ToString() - { - return Id + ", " + Name; - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/Identifiable.cs b/redmine-net20-api/Types/Identifiable.cs deleted file mode 100644 index b4c2d925..00000000 --- a/redmine-net20-api/Types/Identifiable.cs +++ /dev/null @@ -1,74 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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.Xml.Serialization; - -namespace Redmine.Net.Api.Types -{ - /// - /// - /// - /// - public abstract class Identifiable where T : Identifiable - { - private int? oldHashCode; - - /// - /// Gets or sets the id. - /// - /// The id. - [XmlAttribute("id")] - public int Id { get; set; } - - public override bool Equals(object obj) - { - var other = obj as T; - if (other == null) return false; - - var thisIsNew = Equals(Id, default(int)); - var otherIsNew = Equals(other.Id, default(int)); - - if (thisIsNew && otherIsNew) - return ReferenceEquals(this, other); - - return Id.Equals(other.Id); - } - - public override int GetHashCode() - { - if (oldHashCode.HasValue) - return oldHashCode.Value; - - var thisIsNew = Equals(Id, default(int)); - if (thisIsNew) - { - oldHashCode = base.GetHashCode(); - return oldHashCode.Value; - } - return Id.GetHashCode(); - } - - public static bool operator ==(Identifiable left, Identifiable right) - { - return Equals(left, right); - } - - public static bool operator !=(Identifiable left, Identifiable right) - { - return !Equals(left, right); - } - } -} \ 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 100644 index 75b60f2c..00000000 --- a/redmine-net20-api/Types/IdentifiableName.cs +++ /dev/null @@ -1,79 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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.Globalization; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; - -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) - { - ReadXml(reader); - } - - /// - /// Gets or sets the name. - /// - /// The name. - [XmlAttribute("name")] - public String Name { get; set; } - - public XmlSchema GetSchema() { return null; } - - public virtual void ReadXml(XmlReader reader) - { - Id = Convert.ToInt32(reader.GetAttribute("id")); - Name = reader.GetAttribute("name"); - reader.Read(); - } - - public virtual void WriteXml(XmlWriter writer) - { - writer.WriteAttributeString("id", Id.ToString(CultureInfo.InvariantCulture)); - writer.WriteAttributeString("name", Name); - } - - public override string ToString() - { - return Id + ", " + Name; - } - - public bool Equals(IdentifiableName other) - { - if (other == null) return false; - return (Id == other.Id && Name == other.Name); - } - } -} \ 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 bc322773..00000000 --- a/redmine-net20-api/Types/Issue.cs +++ /dev/null @@ -1,406 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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.Globalization; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; - -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("issue")] - public class Issue : Identifiable, IXmlSerializable, IEquatable, ICloneable - { - /// - /// Gets or sets the project. - /// - /// The project. - [XmlElement("project")] - public IdentifiableName Project { get; set; } - - /// - /// Gets or sets the tracker. - /// - /// The tracker. - [XmlElement("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("status")] - public IdentifiableName Status { get; set; } - - /// - /// Gets or sets the priority. - /// - /// The priority. - [XmlElement("priority")] - public IdentifiableName Priority { get; set; } - - /// - /// Gets or sets the author. - /// - /// The author. - [XmlElement("author")] - public IdentifiableName Author { get; set; } - - /// - /// Gets or sets the category. - /// - /// The category. - [XmlElement("category")] - public IdentifiableName Category { get; set; } - - /// - /// Gets or sets the subject. - /// - /// The subject. - [XmlElement("subject")] - public String Subject { get; set; } - - /// - /// Gets or sets the description. - /// - /// The description. - [XmlElement("description")] - public String Description { get; set; } - - /// - /// Gets or sets the start date. - /// - /// The start date. - [XmlElement("start_date", IsNullable = true)] - public DateTime? StartDate { get; set; } - - /// - /// Gets or sets the due date. - /// - /// The due date. - [XmlElement("due_date", IsNullable = true)] - public DateTime? DueDate { get; set; } - - /// - /// Gets or sets the done ratio. - /// - /// The done ratio. - [XmlElement("done_ratio", IsNullable = true)] - public float? DoneRatio { get; set; } - - [XmlElement("private_notes")] - public bool PrivateNotes { get; set; } - - /// - /// Gets or sets the estimated hours. - /// - /// The estimated hours. - [XmlElement("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("spent_hours", IsNullable = true)] - public float? SpentHours { get; set; } - - /// - /// Gets or sets the custom fields. - /// - /// The custom fields. - [XmlArray("custom_fields")] - [XmlArrayItem("custom_field")] - public IList CustomFields { get; set; } - - /// - /// Gets or sets the created on. - /// - /// The created on. - [XmlElement("created_on", IsNullable = true)] - public DateTime? CreatedOn { get; set; } - - /// - /// Gets or sets the updated on. - /// - /// The updated on. - [XmlElement("updated_on", IsNullable = true)] - public DateTime? UpdatedOn { get; set; } - - /// - /// Gets or sets the closed on. - /// - /// The closed on. - [XmlElement("closed_on", IsNullable = true)] - public DateTime? ClosedOn { get; set; } - - /// - /// Gets or sets the notes. - /// - [XmlElement("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("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("parent")] - public IdentifiableName ParentIssue { get; set; } - - /// - /// Gets or sets the fixed version. - /// - /// - /// The fixed version. - /// - [XmlElement("fixed_version")] - public IdentifiableName FixedVersion { get; set; } - - /// - /// indicate whether the issue is private or not - /// - /// - /// true if this issue is private; otherwise, false. - /// - [XmlElement("is_private")] - public bool IsPrivate { get; set; } - /// - /// Gets or sets the journals. - /// - /// - /// The journals. - /// - [XmlArray("journals")] - [XmlArrayItem("journal")] - public IList Journals { get; set; } - - /// - /// Gets or sets the changesets. - /// - /// - /// The changesets. - /// - [XmlArray("changesets")] - [XmlArrayItem("changeset")] - public IList Changesets { get; set; } - - /// - /// Gets or sets the attachments. - /// - /// - /// The attachments. - /// - [XmlArray("attachments")] - [XmlArrayItem("attachment")] - public IList Attachments { get; set; } - - /// - /// Gets or sets the issue relations. - /// - /// - /// The issue relations. - /// - [XmlArray("relations")] - [XmlArrayItem("relation")] - public IList Relations { get; set; } - - /// - /// Gets or sets the issue children. - /// - /// - /// The issue children. - /// NOTE: Only Id, tracker and subject are filled. - /// - [XmlArray("children")] - [XmlArrayItem("issue")] - public IList Children { get; set; } - - /// - /// Gets or sets the attachments. - /// - /// - /// The attachment. - /// - [XmlArray("uploads")] - [XmlArrayItem("upload")] - public IList Uploads { get; set; } - - [XmlArray("watchers")] - [XmlArrayItem("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 "id": Id = reader.ReadElementContentAsInt(); break; - - case "project": Project = new IdentifiableName(reader); break; - - case "tracker": Tracker = new IdentifiableName(reader); break; - - case "status": Status = new IdentifiableName(reader); break; - - case "priority": Priority = new IdentifiableName(reader); break; - - case "author": Author = new IdentifiableName(reader); break; - - case "assigned_to": AssignedTo = new IdentifiableName(reader); break; - - case "category": Category = new IdentifiableName(reader); break; - - case "parent": ParentIssue = new IdentifiableName(reader); break; - - case "fixed_version": FixedVersion = new IdentifiableName(reader); break; - - case "private_notes": - PrivateNotes = reader.ReadElementContentAsBoolean(); - break; - - case "subject": Subject = reader.ReadElementContentAsString(); break; - - case "notes": Notes = reader.ReadElementContentAsString(); break; - - case "description": Description = reader.ReadElementContentAsString(); break; - - case "start_date": StartDate = reader.ReadElementContentAsNullableDateTime(); break; - - case "due_date": DueDate = reader.ReadElementContentAsNullableDateTime(); break; - - case "done_ratio": DoneRatio = reader.ReadElementContentAsNullableFloat(); break; - - case "estimated_hours": EstimatedHours = reader.ReadElementContentAsNullableFloat(); break; - - case "spent_hours": SpentHours = reader.ReadElementContentAsNullableFloat(); break; - - case "created_on": CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case "updated_on": UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case "closed_on": ClosedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case "custom_fields": CustomFields = reader.ReadElementContentAsCollection(); break; - - case "attachments": Attachments = reader.ReadElementContentAsCollection(); break; - - case "relations": Relations = reader.ReadElementContentAsCollection(); break; - - case "journals": Journals = reader.ReadElementContentAsCollection(); break; - - case "changesets": Changesets = reader.ReadElementContentAsCollection(); break; - - case "children": Children = reader.ReadElementContentAsCollection(); break; - - case "watchers": Watchers = reader.ReadElementContentAsCollection(); break; - - default: reader.Read(); break; - } - } - } - - public void WriteXml(XmlWriter writer) - { - writer.WriteElementString("subject", Subject); - writer.WriteElementString("notes", Notes); - if (Id != 0) - { - writer.WriteElementString("private_notes", PrivateNotes.ToString()); - } - writer.WriteElementString("description", Description); - writer.WriteElementString("is_private", IsPrivate.ToString()); - - writer.WriteIdIfNotNull(Project, "project_id"); - writer.WriteIdIfNotNull(Priority, "priority_id"); - writer.WriteIdIfNotNull(Status, "status_id"); - writer.WriteIdIfNotNull(Category, "category_id"); - writer.WriteIdIfNotNull(Tracker, "tracker_id"); - writer.WriteIdIfNotNull(AssignedTo, "assigned_to_id"); - writer.WriteIdIfNotNull(ParentIssue, "parent_issue_id"); - writer.WriteIdIfNotNull(FixedVersion, "fixed_version_id"); - - writer.WriteValue(EstimatedHours, "estimated_hours"); - writer.WriteValue(DoneRatio, "done_ratio"); - writer.WriteDate(StartDate, "start_date"); - writer.WriteDate(DueDate, "due_date"); - writer.WriteDate(UpdatedOn, "updated_on"); - - writer.WriteArray(Uploads, "uploads"); - writer.WriteArray(CustomFields, "custom_fields"); - - // writer.WriteArray(Watchers, "watcher_user_ids"); - - if (Watchers != null) - { - writer.WriteStartElement("watcher_user_ids"); - writer.WriteAttributeString("type", "array"); - foreach (var watcher in Watchers) - { - new XmlSerializer(typeof(int)).Serialize(writer, watcher.Id); - } - writer.WriteEndElement(); - } - } - - public object Clone() - { - var issue = new Issue { AssignedTo = AssignedTo, Author = Author, Category = Category, CustomFields = CustomFields, 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 }; - 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 && Equals(CustomFields, other.CustomFields) - && CreatedOn == other.CreatedOn && UpdatedOn == other.UpdatedOn && AssignedTo == other.AssignedTo && FixedVersion == other.FixedVersion - && Notes == other.Notes && Equals(Watchers, other.Watchers) && ClosedOn == other.ClosedOn && SpentHours == other.SpentHours - && PrivateNotes == other.PrivateNotes - ); - } - } -} \ 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 100644 index a9657c6a..00000000 --- a/redmine-net20-api/Types/IssueCategory.cs +++ /dev/null @@ -1,98 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 1.3 - /// - [XmlRoot("issue_category")] - public class IssueCategory : Identifiable, IEquatable, IXmlSerializable - { - /// - /// Gets or sets the project. - /// - /// - /// The project. - /// - [XmlElement("project ")] - public IdentifiableName Project { get; set; } - - /// - /// Gets or sets the asign to. - /// - /// - /// The asign to. - /// - [XmlElement("assigned_to")] - public IdentifiableName AsignTo { get; set; } - - /// - /// Gets or sets the name. - /// - /// - /// The name. - /// - [XmlElement("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 "id": Id = reader.ReadElementContentAsInt(); break; - - case "project": Project = new IdentifiableName(reader); break; - - case "assigned_to": AsignTo = new IdentifiableName(reader); break; - - case "name": Name = reader.ReadElementContentAsString(); break; - - default: reader.Read(); break; - } - } - } - - public void WriteXml(XmlWriter writer) - { - writer.WriteIdIfNotNull(Project, "project_id"); - writer.WriteElementString("name", Name); - writer.WriteIdIfNotNull(AsignTo, "assigned_to_id"); - } - } -} \ 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 100644 index 310f1dfb..00000000 --- a/redmine-net20-api/Types/IssueChild.cs +++ /dev/null @@ -1,81 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; - -namespace Redmine.Net.Api.Types -{ - [XmlRoot("issue")] - public class IssueChild : Identifiable, IXmlSerializable, IEquatable, ICloneable - { - /// - /// Gets or sets the tracker. - /// - /// The tracker. - [XmlElement("tracker")] - public IdentifiableName Tracker { get; set; } - - /// - /// Gets or sets the subject. - /// - /// The subject. - [XmlElement("subject")] - public String Subject { get; set; } - - public XmlSchema GetSchema() { return null; } - - public void ReadXml(XmlReader reader) - { - Id = Convert.ToInt32(reader.GetAttribute("id")); - reader.Read(); - - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case "tracker": Tracker = new IdentifiableName(reader); break; - - case "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(Issue other) - { - if (other == null) return false; - return (Id == other.Id && Tracker == other.Tracker && Subject == other.Subject); - } - } -} diff --git a/redmine-net20-api/Types/IssueCustomField.cs b/redmine-net20-api/Types/IssueCustomField.cs deleted file mode 100644 index 8f1c22e7..00000000 --- a/redmine-net20-api/Types/IssueCustomField.cs +++ /dev/null @@ -1,87 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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.Globalization; -using System.Xml; -using System.Xml.Serialization; - -namespace Redmine.Net.Api.Types -{ - /// - /// - /// - [XmlRoot("custom_field")] - public class IssueCustomField : IdentifiableName, IEquatable - { - /// - /// Gets or sets the value. - /// - /// The value. - [XmlArray("value")] - [XmlArrayItem("value")] - public IList Values { get; set; } - - [XmlAttribute("multiple")] - public bool Multiple { get; set; } - - public override void ReadXml(XmlReader reader) - { - Id = reader.ReadAttributeAsInt("id"); - Name = reader.GetAttribute("name"); - Multiple = reader.ReadAttributeAsBoolean("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("id", Id.ToString(CultureInfo.InvariantCulture)); - if (itemsCount > 1) - { - writer.WriteStartElement("value"); - writer.WriteAttributeString("type", "array"); - - foreach (var v in Values) writer.WriteElementString("value", v.Info); - - writer.WriteEndElement(); - } - else - { - writer.WriteElementString("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 == other.Values); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/IssuePriority.cs b/redmine-net20-api/Types/IssuePriority.cs deleted file mode 100644 index d9692e5b..00000000 --- a/redmine-net20-api/Types/IssuePriority.cs +++ /dev/null @@ -1,98 +0,0 @@ -/* - Copyright 2011 - 2015 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.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 2.2 - /// - [XmlRoot("issue_priority")] - public class IssuePriority : IXmlSerializable, IEquatable - { - /// - /// Gets or sets the id. - /// - /// - /// The id. - /// - [XmlElement("id")] - public int Id { get; set; } - - /// - /// Gets or sets the name. - /// - /// - /// The name. - /// - [XmlElement("name")] - public string Name { get; set; } - - [XmlElement("is_default")] - public bool IsDefault { get; set; } - - #region Implementation of IXmlSerializable - - public XmlSchema GetSchema() { return null; } - - /// - /// Generates an object from its XML representation. - /// - /// The stream from which the object is deserialized. - public void ReadXml(XmlReader reader) - { - reader.Read(); - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case "id": Id = reader.ReadElementContentAsInt(); break; - - case "name": Name = reader.ReadElementContentAsString(); break; - - case "is_default": IsDefault = reader.ReadElementContentAsBoolean(); break; - - default: reader.Read(); break; - } - } - } - - public void WriteXml(XmlWriter writer) { } - - #endregion - - #region Implementation of IEquatable - - public bool Equals(IssuePriority other) - { - if (other == null) return false; - - return Id == other.Id && Name == other.Name && IsDefault == other.IsDefault; - } - - #endregion - } -} \ 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 100644 index 727db7aa..00000000 --- a/redmine-net20-api/Types/IssueRelation.cs +++ /dev/null @@ -1,126 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 1.3 - /// - [XmlRoot("relation")] - public class IssueRelation : Identifiable, IXmlSerializable, IEquatable - { - /// - /// Gets or sets the issue id. - /// - /// The issue id. - [XmlElement("issue_id")] - public int IssueId { get; set; } - - /// - /// Gets or sets the related issue id. - /// - /// The issue to id. - [XmlElement("issue_to_id")] - public int IssueToId { get; set; } - - /// - /// Gets or sets the type of relation. - /// - /// The type. - [XmlElement("relation_type")] - public IssueRelationType Type { get; set; } - - /// - /// Gets or sets the delay for a "precedes" or "follows" relation. - /// - /// The delay. - [XmlElement("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 "id": Id = reader.ReadAttributeAsInt(attributeName); break; - case "issue_id": IssueId = reader.ReadAttributeAsInt(attributeName); break; - case "issue_to_id": IssueToId = reader.ReadAttributeAsInt(attributeName); break; - case "relation_type": - var rt = reader.GetAttribute(attributeName); - if (!string.IsNullOrEmpty(rt)) - { - Type = (IssueRelationType)Enum.Parse(typeof(IssueRelationType), rt, true); - } - break; - case "delay": Delay = reader.ReadAttributeAsNullableInt(attributeName); break; - } - } - return; - } - - switch (reader.Name) - { - case "id": Id = reader.ReadElementContentAsInt(); break; - case "issue_id": IssueId = reader.ReadElementContentAsInt(); break; - case "issue_to_id": IssueToId = reader.ReadElementContentAsInt(); break; - case "relation_type": - var rt = reader.ReadElementContentAsString(); - if (!string.IsNullOrEmpty(rt)) - { - Type = (IssueRelationType)Enum.Parse(typeof(IssueRelationType), rt, true); - } - break; - case "delay": Delay = reader.ReadElementContentAsNullableInt(); break; - default: reader.Read(); break; - } - } - } - - public void WriteXml(XmlWriter writer) - { - writer.WriteElementString("issue_to_id", IssueToId.ToString()); - writer.WriteElementString("relation_type", Type.ToString()); - if (Type == IssueRelationType.precedes || Type == IssueRelationType.follows) - writer.WriteValue(Delay, "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); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/IssueRelationType.cs b/redmine-net20-api/Types/IssueRelationType.cs deleted file mode 100644 index 64706123..00000000 --- a/redmine-net20-api/Types/IssueRelationType.cs +++ /dev/null @@ -1,15 +0,0 @@ -ο»Ώnamespace Redmine.Net.Api.Types -{ - public enum IssueRelationType - { - relates = 1, - duplicates, - duplicated, - blocks, - blocked, - precedes, - follows, - copied_to, - copied_from - } -} \ 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 100644 index 9ef1207f..00000000 --- a/redmine-net20-api/Types/IssueStatus.cs +++ /dev/null @@ -1,79 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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.Xml; -using System.Xml.Serialization; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 1.3 - /// - [XmlRoot("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("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("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 "id": Id = reader.ReadElementContentAsInt(); break; - - case "name": Name = reader.ReadElementContentAsString(); break; - - case "is_default": IsDefault = reader.ReadElementContentAsBoolean(); break; - - case "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); - } - } -} \ 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 100644 index 35c2aa5f..00000000 --- a/redmine-net20-api/Types/Journal.cs +++ /dev/null @@ -1,115 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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; - -namespace Redmine.Net.Api.Types -{ - /// - /// - /// - [XmlRoot("journal")] - public class Journal : IXmlSerializable, IEquatable - { - /// - /// Gets or sets the id. - /// - /// - /// The id. - /// - [XmlAttribute("id")] - public int Id { get; set; } - - /// - /// Gets or sets the user. - /// - /// - /// The user. - /// - [XmlElement("user")] - public IdentifiableName User { get; set; } - - /// - /// Gets or sets the notes. - /// - /// - /// The notes. - /// - [XmlElement("notes")] - public string Notes { get; set; } - - /// - /// Gets or sets the created on. - /// - /// - /// The created on. - /// - [XmlElement("created_on", IsNullable = true)] - public DateTime? CreatedOn { get; set; } - - /// - /// Gets or sets the details. - /// - /// - /// The details. - /// - [XmlArray("details")] - [XmlArrayItem("detail")] - public IList Details { get; set; } - - public XmlSchema GetSchema() { return null; } - - public void ReadXml(XmlReader reader) - { - Id = reader.ReadAttributeAsInt("id"); - reader.Read(); - - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case "user": User = new IdentifiableName(reader); break; - - case "notes": Notes = reader.ReadElementContentAsString(); break; - - case "created_on": CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case "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 == other.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 100644 index 386e77b3..00000000 --- a/redmine-net20-api/Types/Membership.cs +++ /dev/null @@ -1,80 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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; - -namespace Redmine.Net.Api.Types -{ - /// - /// Only the roles can be updated, the project and the user of a membership are read-only. - /// - [XmlRoot("membership")] - public class Membership : Identifiable, IEquatable, IXmlSerializable - { - /// - /// Gets or sets the project. - /// - /// The project. - [XmlElement("project")] - public IdentifiableName Project { get; set; } - - /// - /// Gets or sets the type. - /// - /// The type. - [XmlArray("roles")] - [XmlArrayItem("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 "id": Id = reader.ReadElementContentAsInt();break; - - case "project": Project = new IdentifiableName(reader); break; - - case "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 == other.Project && Roles == other.Roles); - } - } -} \ 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 100644 index 36c3726b..00000000 --- a/redmine-net20-api/Types/MembershipRole.cs +++ /dev/null @@ -1,66 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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.Xml; -using System.Xml.Serialization; - -namespace Redmine.Net.Api.Types -{ - /// - /// - /// - [XmlRoot("role")] - public class MembershipRole : IdentifiableName, IEquatable - { - /// - /// Gets or sets a value indicating whether this is inherited. - /// - /// - /// true if inherited; otherwise, false. - /// - [XmlAttribute("inherited")] - public bool Inherited { get; set; } - - /// - /// Reads the XML. - /// - /// The reader. - public override void ReadXml(XmlReader reader) - { - Id = Convert.ToInt32(reader.GetAttribute("id")); - Name = reader.GetAttribute("name"); - Inherited = reader.ReadAttributeAsBoolean("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 string ToString() - { - return Id + ", " + Name + ", " + Inherited; - } - } -} \ 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 100644 index b62c3337..00000000 --- a/redmine-net20-api/Types/News.cs +++ /dev/null @@ -1,114 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 1.1 - /// - [XmlRoot("news")] - public class News : Identifiable, IEquatable, IXmlSerializable - { - /// - /// Gets or sets the project. - /// - /// The project. - [XmlElement("project")] - public IdentifiableName Project { get; set; } - - /// - /// Gets or sets the author. - /// - /// The author. - [XmlElement("author")] - public IdentifiableName Author { get; set; } - - /// - /// Gets or sets the title. - /// - /// The title. - [XmlElement("title")] - public String Title { get; set; } - - /// - /// Gets or sets the summary. - /// - /// The summary. - [XmlElement("summary")] - public String Summary { get; set; } - - /// - /// Gets or sets the description. - /// - /// The description. - [XmlElement("description")] - public String Description { get; set; } - - /// - /// Gets or sets the created on. - /// - /// The created on. - [XmlElement("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 "id": Id = reader.ReadElementContentAsInt(); break; - - case "project": Project = new IdentifiableName(reader); break; - - case "author": Author = new IdentifiableName(reader); break; - - case "title": Title = reader.ReadElementContentAsString(); break; - - case "summary": Summary = reader.ReadElementContentAsString(); break; - - case "description": Description = reader.ReadElementContentAsString(); break; - - case "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); - } - } -} \ 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 b9cd74a9..00000000 --- a/redmine-net20-api/Types/Project.cs +++ /dev/null @@ -1,200 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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.Serialization; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 1.0 - /// - [XmlRoot("project")] - // [DataContract(Name = "project")] - public class Project : IdentifiableName, IEquatable - { - /// - /// Gets or sets the identifier. - /// - /// The identifier. - [XmlElement("identifier")] - public String Identifier { get; set; } - - /// - /// Gets or sets the description. - /// - /// The description. - [XmlElement("description")] - public String Description { get; set; } - - /// - /// Gets or sets the parent. - /// - /// The parent. - [XmlElement("parent")] - public IdentifiableName Parent { get; set; } - - /// - /// Gets or sets the home page. - /// - /// The home page. - [XmlElement("homepage")] - public String HomePage { get; set; } - - /// - /// Gets or sets the created on. - /// - /// The created on. - [XmlElement("created_on", IsNullable = true)] - public DateTime? CreatedOn { get; set; } - - /// - /// Gets or sets the updated on. - /// - /// The updated on. - [XmlElement("updated_on", IsNullable = true)] - public DateTime? UpdatedOn { get; set; } - - [XmlElement("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("is_public")] - public bool IsPublic { get; set; } - - [XmlElement("inherit_members")] - public bool InheritMembers { get; set; } - - /// - /// Gets or sets the trackers. - /// - /// - /// The trackers. - /// - [XmlArray("trackers")] - [XmlArrayItem("tracker")] - public IList Trackers { get; set; } - - [XmlArray("custom_fields")] - [XmlArrayItem("custom_field")] - public IList CustomFields { get; set; } - - [XmlArray("issue_categories")] - [XmlArrayItem("issue_category")] - public IList IssueCategories { get; set; } - - /// - /// since 2.6.0 - /// - [XmlArray("enabled_modules")] - [XmlArrayItem("enabled_module")] - public IList EnabledModules { 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 "id": Id = reader.ReadElementContentAsInt(); break; - - case "name": Name = reader.ReadElementContentAsString(); break; - - case "identifier": Identifier = reader.ReadElementContentAsString(); break; - - case "description": Description = reader.ReadElementContentAsString(); break; - - case "status": Status = (ProjectStatus)reader.ReadElementContentAsInt(); break; - - case "parent": Parent = new IdentifiableName(reader); break; - - case "homepage": HomePage = reader.ReadElementContentAsString(); break; - - case "is_public": IsPublic = reader.ReadElementContentAsBoolean(); break; - - case "inherit_members": InheritMembers = reader.ReadElementContentAsBoolean(); break; - - case "created_on": CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case "updated_on": UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case "trackers": Trackers = reader.ReadElementContentAsCollection(); break; - - case "custom_fields": CustomFields = reader.ReadElementContentAsCollection(); break; - - case "issue_categories": IssueCategories = reader.ReadElementContentAsCollection(); break; - - case "enabled_modules": EnabledModules = reader.ReadElementContentAsCollection(); break; - default: reader.Read(); break; - } - } - } - - public override void WriteXml(XmlWriter writer) - { - writer.WriteElementString("name", Name); - writer.WriteElementString("identifier", Identifier); - writer.WriteElementString("description", Description); - writer.WriteElementString("inherit_members", InheritMembers.ToString()); - writer.WriteElementString("is_public", IsPublic.ToString()); - writer.WriteIdIfNotNull(Parent, "parent_id"); - writer.WriteElementString("homepage", HomePage); - - if (EnabledModules != null) - { - var enabledModuleNames = ""; - foreach (var projectEnabledModule in EnabledModules) - { - if (!string.IsNullOrEmpty(projectEnabledModule.Name)) - { - enabledModuleNames += projectEnabledModule.Name; - } - } - - writer.WriteElementString("enabled_module_names", enabledModuleNames); - } - - if (Id == 0) return; - - writer.WriteArray(CustomFields, "custom_fields"); - } - - public bool Equals(Project other) - { - if (other == null) return false; - return (Id == other.Id && Identifier == other.Identifier); - } - } -} \ 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 100644 index 163c8d74..00000000 --- a/redmine-net20-api/Types/ProjectMembership.cs +++ /dev/null @@ -1,117 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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; - -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("membership")] - public class ProjectMembership : Identifiable, IEquatable, IXmlSerializable - { - /// - /// Gets or sets the project. - /// - /// The project. - [XmlElement("project")] - public IdentifiableName Project { get; set; } - - /// - /// Gets or sets the user. - /// - /// - /// The user. - /// - [XmlElement("user")] - public IdentifiableName User { get; set; } - - /// - /// Gets or sets the group. - /// - /// - /// The group. - /// - [XmlElement("group")] - public IdentifiableName Group { get; set; } - - /// - /// Gets or sets the type. - /// - /// The type. - [XmlArray("roles")] - [XmlArrayItem("role")] - public List Roles { get; set; } - - public bool Equals(ProjectMembership other) - { - if (other == null) return false; - return (Id == other.Id && Project == other.Project && Roles == other.Roles && User == other.User && Group == other.Group); - } - - 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 "id": Id = reader.ReadElementContentAsInt(); break; - - case "project": Project = new IdentifiableName(reader); break; - - case "user": User = new IdentifiableName(reader); break; - - case "group": Group = new IdentifiableName(reader); break; - - case "roles": Roles = reader.ReadElementContentAsCollection(); break; - - default: reader.Read(); break; - } - } - } - - public void WriteXml(XmlWriter writer) - { - writer.WriteIdIfNotNull(User, "user_id"); - - writer.WriteStartElement("role_ids"); - writer.WriteAttributeString("type", "array"); - foreach (var role in Roles) - { - new XmlSerializer(role.GetType(), new XmlAttributeOverrides(), null, new XmlRootAttribute("role_id"), "").Serialize(writer, role); - } - writer.WriteEndElement(); - } - } -} \ 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 100644 index bbc4f579..00000000 --- a/redmine-net20-api/Types/Query.cs +++ /dev/null @@ -1,78 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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.Xml; -using System.Xml.Serialization; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 1.3 - /// - [XmlRoot("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("is_public")] - public bool IsPublic { get; set; } - - /// - /// Gets or sets the project id. - /// - /// The project id. - [XmlElement("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 "id": Id = reader.ReadElementContentAsInt(); break; - - case "name": Name = reader.ReadElementContentAsString(); break; - - case "is_public": IsPublic = reader.ReadElementContentAsBoolean(); break; - - case "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); - } - } -} \ 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 100644 index 03924a56..00000000 --- a/redmine-net20-api/Types/Role.cs +++ /dev/null @@ -1,98 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 1.4 - /// - [XmlRoot("role")] - public class Role : IXmlSerializable, IEquatable - { - /// - /// Gets or sets the id. - /// - /// - /// The id. - /// - [XmlElement("id")] - public int Id { get; set; } - - /// - /// Gets or sets the name. - /// - /// - /// The name. - /// - [XmlElement("name")] - public string Name { get; set; } - - /// - /// Gets or sets the permissions. - /// - /// - /// The issue relations. - /// - [XmlArray("permissions")] - [XmlArrayItem("permission")] - public IList Permissions { 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 "id": Id = reader.ReadElementContentAsInt(); break; - - case "name": Name = reader.ReadElementContentAsString(); break; - - case "permissions": Permissions = reader.ReadElementContentAsCollection(); break; - - default: reader.Read(); break; - } - } - } - - public void WriteXml(XmlWriter writer){} - - public bool Equals(Role other) - { - if (other == null) return false; - return Id == other.Id && Name == other.Name; - } - - public override string ToString() - { - return Id + ", " + Name; - } - } -} \ 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 100644 index 9ea3da2a..00000000 --- a/redmine-net20-api/Types/TimeEntry.cs +++ /dev/null @@ -1,190 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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.Globalization; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 1.1 - /// - [XmlRoot("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("issue")] - public IdentifiableName Issue { get; set; } - - /// - /// Gets or sets the project id to log time on. - /// - /// The project id. - [XmlAttribute("project")] - public IdentifiableName Project { get; set; } - - /// - /// Gets or sets the date the time was spent (default to the current date). - /// - /// The spent on. - [XmlAttribute("spent_on")] - public DateTime? SpentOn { get; set; } - - /// - /// Gets or sets the number of spent hours. - /// - /// The hours. - [XmlAttribute("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("activity")] - public IdentifiableName Activity { get; set; } - - /// - /// Gets or sets the user. - /// - /// - /// The user. - /// - [XmlAttribute("user")] - public IdentifiableName User { get; set; } - - /// - /// Gets or sets the short description for the entry (255 characters max). - /// - /// The comments. - [XmlAttribute("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("created_on")] - public DateTime? CreatedOn { get; set; } - - /// - /// Gets or sets the updated on. - /// - /// The updated on. - [XmlElement("updated_on")] - public DateTime? UpdatedOn { get; set; } - - /// - /// Gets or sets the custom fields. - /// - /// The custom fields. - [XmlArray("custom_fields")] - [XmlArrayItem("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 "id": Id = reader.ReadElementContentAsInt(); break; - - case "issue_id": Issue = new IdentifiableName(reader); break; - case "issue": Issue = new IdentifiableName(reader); break; - - case "project_id": Project = new IdentifiableName(reader); break; - case "project": Project = new IdentifiableName(reader); break; - - case "spent_on": SpentOn = reader.ReadElementContentAsNullableDateTime(); break; - - case "user": User = new IdentifiableName(reader); break; - - case "hours": Hours = reader.ReadElementContentAsDecimal(); break; - - case "activity_id": Activity = new IdentifiableName(reader); break; - case "activity": Activity = new IdentifiableName(reader); break; - - case "comments": Comments = reader.ReadElementContentAsString(); break; - - case "created_on": CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case "updated_on": UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case "custom_fields": CustomFields = reader.ReadElementContentAsCollection(); break; - - default: reader.Read(); break; - } - } - } - - public void WriteXml(XmlWriter writer) - { - writer.WriteIdIfNotNull(Issue, "issue_id"); - writer.WriteIdIfNotNull(Project, "project_id"); - if (!SpentOn.HasValue) SpentOn = DateTime.Now; - writer.WriteDate(SpentOn, "spent_on"); - writer.WriteValue(Hours, "hours"); - writer.WriteIdIfNotNull(Activity, "activity_id"); - writer.WriteElementString("comments", Comments); - } - - 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 == other.CustomFields); - } - } -} \ 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 100644 index 28ba7726..00000000 --- a/redmine-net20-api/Types/TimeEntryActivity.cs +++ /dev/null @@ -1,103 +0,0 @@ -/* - Copyright 2011 - 2015 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.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 2.2 - /// - [XmlRoot("time_entry_activity")] - public class TimeEntryActivity : IXmlSerializable, IEquatable - { - /// - /// Gets or sets the id. - /// - /// - /// The id. - /// - [XmlElement("id")] - public int Id { get; set; } - - /// - /// Gets or sets the name. - /// - /// - /// The name. - /// - [XmlElement("name")] - public string Name { get; set; } - - [XmlElement("is_default")] - public bool IsDefault { get; set; } - - #region Implementation of IXmlSerializable - - public XmlSchema GetSchema() { return null; } - - /// - /// Generates an object from its XML representation. - /// - /// The stream from which the object is deserialized. - public void ReadXml(XmlReader reader) - { - reader.Read(); - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case "id": Id = reader.ReadElementContentAsInt(); break; - - case "name": Name = reader.ReadElementContentAsString(); break; - - case "is_default": IsDefault = reader.ReadElementContentAsBoolean(); break; - - default: reader.Read(); break; - } - } - } - - public 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; - } - - #endregion - - public override string ToString() - { - return Id + ", " + Name; - } - } -} \ 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 100644 index c874fed3..00000000 --- a/redmine-net20-api/Types/Tracker.cs +++ /dev/null @@ -1,97 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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.Xml; -using System.Xml.Serialization; -using System.Xml.Schema; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 1.3 - /// - [XmlRoot("tracker")] - public class Tracker : IXmlSerializable, IEquatable - { - /// - /// Gets or sets the id. - /// - /// - /// The id. - /// - [XmlElement("id")] - public int Id { get; set; } - - /// - /// Gets or sets the name. - /// - /// - /// The name. - /// - [XmlElement("name")] - public string Name { get; set; } - - public void WriteXml(XmlWriter writer) { } - - public XmlSchema GetSchema() { return null; } - - /// - /// Generates an object from its XML representation. - /// - /// The stream from which the object is deserialized. - public virtual void ReadXml(XmlReader reader) - { - reader.Read(); - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case "id": Id = reader.ReadElementContentAsInt(); break; - - case "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 string ToString() - { - return 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 100644 index 0844098f..00000000 --- a/redmine-net20-api/Types/Upload.cs +++ /dev/null @@ -1,65 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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.Xml.Schema; -using System.Xml.Serialization; - -namespace Redmine.Net.Api.Types -{ - /// - /// Support for adding attachments through the REST API is added in Redmine 1.4.0. - /// - [XmlRoot("upload")] - public class Upload : IEquatable - { - /// - /// Gets or sets the uploaded token. - /// - /// The name of the file. - [XmlElement("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("filename")] - public string FileName { get; set; } - - /// - /// Gets or sets the name of the file. - /// - /// The name of the file. - [XmlElement("content_type")] - public string ContentType { get; set; } - - /// - /// Gets or sets the file description. (Undocumented feature) - /// - /// The file descroΓΌtopm. - [XmlElement("description")] - public string Description { get; set; } - - public XmlSchema GetSchema() { return null; } - - public bool Equals(Upload other) - { - return other != null && Token == other.Token; - } - } -} \ 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 100644 index 1d422bad..00000000 --- a/redmine-net20-api/Types/User.cs +++ /dev/null @@ -1,216 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 1.1 - /// - [XmlRoot("user")] - public class User : Identifiable, IXmlSerializable, IEquatable - { - /// - /// Gets or sets the user login. - /// - /// The login. - [XmlElement("login")] - public String Login { get; set; } - - /// - /// Gets or sets the user password. - /// - /// The password. - [XmlElement("password")] - public string Password { get; set; } - - /// - /// Gets or sets the first name. - /// - /// The first name. - [XmlElement("firstname")] - public String FirstName { get; set; } - - /// - /// Gets or sets the last name. - /// - /// The last name. - [XmlElement("lastname")] - public String LastName { get; set; } - - /// - /// Gets or sets the email. - /// - /// The email. - [XmlElement("mail")] - public String Email { get; set; } - - /// - /// Gets or sets the authentication mode id. - /// - /// - /// The authentication mode id. - /// - [XmlElement("auth_source_id", IsNullable = true)] - public Int32? AuthenticationModeId { get; set; } - - /// - /// Gets or sets the created on. - /// - /// The created on. - [XmlElement("created_on", IsNullable = true)] - public DateTime? CreatedOn { get; set; } - - /// - /// Gets or sets the last login on. - /// - /// The last login on. - [XmlElement("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("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("status", IsNullable = true)] - public UserStatus Status { get; set; } - - [XmlElement("must_change_passwd", IsNullable = true)] - public bool MustChangePassword { get; set; } - - /// - /// Gets or sets the custom fields. - /// - /// The custom fields. - [XmlArray("custom_fields")] - [XmlArrayItem("custom_field")] - public List CustomFields { get; set; } - - /// - /// Gets or sets the memberships. - /// - /// - /// The memberships. - /// - [XmlArray("memberships")] - [XmlArrayItem("membership")] - public List Memberships { get; set; } - - /// - /// Gets or sets the user's groups. - /// - /// - /// The groups. - /// - [XmlArray("groups")] - [XmlArrayItem("group")] - public List Groups { 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 "id": Id = reader.ReadElementContentAsInt(); break; - - case "login": Login = reader.ReadElementContentAsString(); break; - - case "firstname": FirstName = reader.ReadElementContentAsString(); break; - - case "lastname": LastName = reader.ReadElementContentAsString(); break; - - case "mail": Email = reader.ReadElementContentAsString(); break; - - case "must_change_passwd": MustChangePassword = reader.ReadElementContentAsBoolean(); break; - - case "auth_source_id": AuthenticationModeId = reader.ReadElementContentAsNullableInt(); break; - - case "last_login_on": LastLoginOn = reader.ReadElementContentAsNullableDateTime(); break; - - case "created_on": CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case "api_key": ApiKey = reader.ReadElementContentAsString(); break; - - case "status": Status = (UserStatus)reader.ReadElementContentAsInt(); break; - - case "custom_fields": CustomFields = reader.ReadElementContentAsCollection(); break; - - case "memberships": Memberships = reader.ReadElementContentAsCollection(); break; - - case "groups": Groups = reader.ReadElementContentAsCollection(); break; - - default: reader.Read(); break; - } - } - } - - public void WriteXml(XmlWriter writer) - { - writer.WriteElementString("login", Login); - writer.WriteElementString("firstname", FirstName); - writer.WriteElementString("lastname", LastName); - writer.WriteElementString("mail", Email); - writer.WriteElementString("password", Password); - writer.WriteValue(AuthenticationModeId, "auth_source_id"); - writer.WriteElementString("must_change_passwd", MustChangePassword.ToString()); - - writer.WriteArray(CustomFields, "custom_fields"); - } - - public bool Equals(User other) - { - if (other == null) return false; - return (Id == other.Id - && AuthenticationModeId == other.AuthenticationModeId - && Login == other.Login - && Password == other.Password - && FirstName == other.FirstName - && LastName == other.LastName - && Email == other.Email - && MustChangePassword == other.MustChangePassword - && CreatedOn == other.CreatedOn - && LastLoginOn == other.LastLoginOn - && CustomFields == other.CustomFields - && Memberships == other.Memberships - && Status == other.Status - && Groups == other.Groups - && ApiKey == other.ApiKey); - } - } -} \ No newline at end of file diff --git a/redmine-net20-api/Types/UserGroup.cs b/redmine-net20-api/Types/UserGroup.cs deleted file mode 100644 index 19d98a70..00000000 --- a/redmine-net20-api/Types/UserGroup.cs +++ /dev/null @@ -1,36 +0,0 @@ -/* - Copyright 2011 - 2015 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.Xml.Serialization; - -namespace Redmine.Net.Api.Types -{ - [XmlRoot("group")] - public class UserGroup : IdentifiableName, IEquatable - { - public bool Equals(UserGroup other) - { - if (other == null) return false; - return Id == other.Id && Name == other.Name; - } - - public override string ToString() - { - return Id + ", " + Name; - } - } -} \ 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 100644 index 7acc9040..00000000 --- a/redmine-net20-api/Types/Version.cs +++ /dev/null @@ -1,158 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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.Globalization; -using System.Xml; -using System.Xml.Serialization; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 1.3 - /// - [XmlRoot("version")] - public class Version : IdentifiableName, IEquatable - { - /// - /// Gets or sets the project. - /// - /// The project. - [XmlElement("project")] - public IdentifiableName Project { get; set; } - - /// - /// Gets or sets the description. - /// - /// The description. - [XmlElement("description")] - public String Description { get; set; } - - /// - /// Gets or sets the status. - /// - /// The status. - [XmlElement("status")] - public VersionStatus Status { get; set; } - - /// - /// Gets or sets the due date. - /// - /// The due date. - [XmlElement("due_date", IsNullable = true)] - public DateTime? DueDate { get; set; } - - /// - /// Gets or sets the sharing. - /// - /// The sharing. - [XmlElement("sharing")] - public VersionSharing Sharing { get; set; } - - /// - /// Gets or sets the created on. - /// - /// The created on. - [XmlElement("created_on", IsNullable = true)] - public DateTime? CreatedOn { get; set; } - - /// - /// Gets or sets the updated on. - /// - /// The updated on. - [XmlElement("updated_on", IsNullable = true)] - public DateTime? UpdatedOn { get; set; } - - /// - /// Gets or sets the custom fields. - /// - /// The custom fields. - [XmlArray("custom_fields")] - [XmlArrayItem("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 "id": Id = reader.ReadElementContentAsInt(); break; - - case "name": Name = reader.ReadElementContentAsString(); break; - - case "project": Project = new IdentifiableName(reader); break; - - case "description": Description = reader.ReadElementContentAsString(); break; - - case "status": Status = (VersionStatus)Enum.Parse(typeof(VersionStatus), reader.ReadElementContentAsString(), true); break; - - case "due_date": DueDate = reader.ReadElementContentAsNullableDateTime(); break; - - case "sharing": Sharing = (VersionSharing)Enum.Parse(typeof(VersionSharing), reader.ReadElementContentAsString(), true); break; - - case "created_on": CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case "updated_on": UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case "custom_fields": CustomFields = reader.ReadElementContentAsCollection(); break; - - default: reader.Read(); break; - } - } - } - - public override void WriteXml(XmlWriter writer) - { - writer.WriteElementString("name", Name); - writer.WriteElementString("status", Status.ToString()); - writer.WriteElementString("sharing", Sharing.ToString()); - - writer.WriteDate(DueDate, "due_date"); - writer.WriteElementString("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 == other.CustomFields); - } - } - - 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 b05e5efc..00000000 --- a/redmine-net20-api/Types/WikiPage.cs +++ /dev/null @@ -1,130 +0,0 @@ -/* - Copyright 2011 - 2015 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; - -namespace Redmine.Net.Api.Types -{ - /// - /// Availability 2.2 - /// - [XmlRoot("wiki_page")] - public class WikiPage : Identifiable, IXmlSerializable, IEquatable - { - [XmlElement("title")] - public string Title { get; set; } - - [XmlElement("text")] - public string Text { get; set; } - - [XmlElement("comments")] - public string Comments { get; set; } - - [XmlElement("version")] - public int Version { get; set; } - - [XmlElement("author")] - public IdentifiableName Author { get; set; } - - /// - /// Gets or sets the created on. - /// - /// The created on. - [XmlElement("created_on")] - public DateTime? CreatedOn { get; set; } - - /// - /// Gets or sets the updated on. - /// - /// The updated on. - [XmlElement("updated_on")] - public DateTime? UpdatedOn { get; set; } - - /// - /// Gets or sets the attachments. - /// - /// - /// The attachments. - /// - [XmlArray("attachments")] - [XmlArrayItem("attachment")] - public IList Attachments { 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 "id": Id = reader.ReadElementContentAsInt(); break; - - case "title": Title = reader.ReadElementContentAsString(); break; - - case "text": Text = reader.ReadElementContentAsString(); break; - - case "comments": Comments = reader.ReadElementContentAsString(); break; - - case "version": Version = reader.ReadElementContentAsInt(); break; - - case "author": Author = new IdentifiableName(reader); break; - - case "created_on": CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case "updated_on": UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case "attachments": Attachments = reader.ReadElementContentAsCollection(); break; - - default: reader.Read(); break; - } - } - } - - public void WriteXml(XmlWriter writer) - { - writer.WriteElementString("text", Text); - writer.WriteElementString("comments", Comments); - writer.WriteValue(Version,"version"); - } - - #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; - } - - #endregion - } -} \ No newline at end of file diff --git a/redmine-net20-api/XmlStreamingDeserializer.cs b/redmine-net20-api/XmlStreamingDeserializer.cs deleted file mode 100644 index 233ead14..00000000 --- a/redmine-net20-api/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 = new XmlSerializer(typeof(T)); - 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/XmlStreamingSerializer.cs b/redmine-net20-api/XmlStreamingSerializer.cs deleted file mode 100644 index 56549eee..00000000 --- a/redmine-net20-api/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/redmine-net20-api.csproj b/redmine-net20-api/redmine-net20-api.csproj deleted file mode 100644 index 0ceec2a8..00000000 --- a/redmine-net20-api/redmine-net20-api.csproj +++ /dev/null @@ -1,103 +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 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - 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 100644 index 2b30b1cd..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 - 2015")] -[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.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] 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 edd48ef0..00000000 --- a/redmine-net40-api-signed/redmine-net40-api-signed.csproj +++ /dev/null @@ -1,336 +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 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - true - - - redmine-net-api.snk - - - - - - - - - - - - - - Types\Attachment.cs - - - Types\ChangeSet.cs - - - Types\CustomField.cs - - - Types\CustomFieldPossibleValue.cs - - - Types\CustomFieldRole.cs - - - Types\CustomFieldValue.cs - - - Types\Detail.cs - - - Types\Error.cs - - - Types\Group.cs - - - Types\GroupUser.cs - - - Types\Identifiable.cs - - - Types\IdentifiableName.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\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 - - - ExtensionMethods.cs - - - JsonConverters\AttachmentConverter.cs - - - JsonConverters\ChangeSetConverter.cs - - - JsonConverters\CustomFieldConverter.cs - - - JsonConverters\CustomFieldPossibleValueConverter.cs - - - JsonConverters\CustomFieldRoleConverter.cs - - - JsonConverters\DetailConverter.cs - - - JsonConverters\ErrorConverter.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 - - - RedmineException.cs - - - RedmineManager.cs - - - RedmineManagerAsync.cs - - - RedmineSerialization.cs - - - RedmineSerializationJSON.cs - - - RedmineWebClient.cs - Component - - - - - - - - - diff --git a/redmine-net40-api/ExtensionMethods.cs b/redmine-net40-api/ExtensionMethods.cs deleted file mode 100644 index 24bf5913..00000000 --- a/redmine-net40-api/ExtensionMethods.cs +++ /dev/null @@ -1,358 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Web.Script.Serialization; -using System.Xml; -using System.Xml.Serialization; -using Redmine.Net.Api.JSonConverters; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api -{ - public static class ExtensionMethods - { - /// - /// Reads the attribute as int. - /// - /// The reader. - /// Name of the attribute. - /// - public static int ReadAttributeAsInt(this XmlReader reader, string attributeName) - { - try - { - var attribute = reader.GetAttribute(attributeName); - int result; - if (String.IsNullOrWhiteSpace(attribute) || !Int32.TryParse(attribute, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out result)) return default(int); - return result; - } - catch - { - return -1; - } - } - - public static int? ReadAttributeAsNullableInt(this XmlReader reader, string attributeName) - { - try - { - var attribute = reader.GetAttribute(attributeName); - int result; - if (String.IsNullOrWhiteSpace(attribute) || !Int32.TryParse(attribute, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out result)) return null; - return result; - } - catch - { - return null; - } - } - - /// - /// Reads the attribute as boolean. - /// - /// The reader. - /// Name of the attribute. - /// - public static bool ReadAttributeAsBoolean(this XmlReader reader, string attributeName) - { - try - { - var attribute = reader.GetAttribute(attributeName); - bool result; - if (String.IsNullOrWhiteSpace(attribute) || !Boolean.TryParse(attribute, out result)) return false; - - return result; - } - catch - { - return false; - } - } - - /// - /// 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.IsNullOrWhiteSpace(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.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; - } - - 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; - } - - /// - /// 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)); - } - - public static void WriteIdIfNotNull(this Dictionary dictionary, IdentifiableName ident, String key) - { - if (ident != null) dictionary.Add(key, ident.Id); - } - - /// - /// Writes string empty if T has default value or null. - /// - /// - /// The writer. - /// The value. - /// The tag. - public static void WriteValue(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)); - } - - public static void WriteDate(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))); - } - - public static void WriteArray(this XmlWriter writer, IEnumerable col, string elementName) - { - - writer.WriteStartElement(elementName); - writer.WriteAttributeString("type", "array"); - if (col != null) - { - foreach (var item in col) - { - new XmlSerializer(item.GetType()).Serialize(writer, item); - } - } - writer.WriteEndElement(); - } - - public static void WriteIfNotDefaultOrNull(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); - } - - 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); - } - - 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 - /// - /// - /// - /// - /// - 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[] { RedmineSerialization.Converters[typeof(T)] }); - - List list = new List(); - - var arrayList = val as ArrayList; - if (arrayList != null) - { - foreach (var item in arrayList) - { - var type = ser.ConvertToType(item); - list.Add(type); - } - } - else - { - var dict = val as Dictionary; - if (dict != null) - { - foreach (var pair in dict) - { - var type = ser.ConvertToType(pair.Value); - list.Add(type); - } - - } - } - return list; - } - - } -} \ 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 100644 index b31e5217..00000000 --- a/redmine-net40-api/JSonConverters/AttachmentConverter.cs +++ /dev/null @@ -1,55 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class AttachmentConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var attachment = new Attachment(); - - attachment.Id = dictionary.GetValue("id"); - attachment.Description = dictionary.GetValue("description"); - attachment.Author = dictionary.GetValueAsIdentifiableName("author"); - attachment.ContentType = dictionary.GetValue("content_type"); - attachment.ContentUrl = dictionary.GetValue("content_url"); - attachment.CreatedOn = dictionary.GetValue("created_on"); - attachment.FileName = dictionary.GetValue("filename"); - attachment.FileSize = dictionary.GetValue("filesize"); - - return attachment; - } - - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) { return null; } - - 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/ChangeSetConverter.cs b/redmine-net40-api/JSonConverters/ChangeSetConverter.cs deleted file mode 100644 index ffb2e6e4..00000000 --- a/redmine-net40-api/JSonConverters/ChangeSetConverter.cs +++ /dev/null @@ -1,53 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ChangeSetConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var changeSet = new ChangeSet - { - Revision = dictionary.GetValue("revision"), - Comments = dictionary.GetValue("comments"), - User = dictionary.GetValueAsIdentifiableName("user"), - CommittedOn = dictionary.GetValue("committed_on") - }; - - - return changeSet; - } - - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) { return null; } - - 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 c47055cc..00000000 --- a/redmine-net40-api/JSonConverters/CustomFieldConverter.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class CustomFieldConverter : IdentifiableNameConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var customField = new CustomField(); - - customField.Id = dictionary.GetValue("id"); - customField.Name = dictionary.GetValue("name"); - customField.CustomizedType = dictionary.GetValue("customized_type"); - customField.FieldFormat = dictionary.GetValue("field_format"); - customField.Regexp = dictionary.GetValue("regexp"); - customField.MinLength = dictionary.GetValue("min_length"); - customField.MaxLength = dictionary.GetValue("max_length"); - customField.IsRequired = dictionary.GetValue("is_required"); - customField.IsFilter = dictionary.GetValue("is_filter"); - customField.Searchable = dictionary.GetValue("searchable"); - customField.Multiple = dictionary.GetValue("multiple"); - customField.DefaultValue = dictionary.GetValue("default_value"); - customField.Visible = dictionary.GetValue("visible"); - customField.PossibleValues = dictionary.GetValueAsCollection("possible_values"); - customField.Trackers = dictionary.GetValueAsCollection("trackers"); - customField.Roles = dictionary.GetValueAsCollection("roles"); - - - return customField; - } - - return null; - } - - 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 182279b9..00000000 --- a/redmine-net40-api/JSonConverters/CustomFieldPossibleValueConverter.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class CustomFieldPossibleValueConverter : IdentifiableNameConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var entity = new CustomFieldPossibleValue(); - - entity.Value = dictionary.GetValue("value"); - - return entity; - } - - return null; - } - - 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 90995780..00000000 --- a/redmine-net40-api/JSonConverters/CustomFieldRoleConverter.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class CustomFieldRoleConverter : IdentifiableNameConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var entity = new CustomFieldRole(); - - entity.Id = dictionary.GetValue("id"); - entity.Name = dictionary.GetValue("name"); - - return entity; - } - - return null; - } - - 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 100644 index 10f639f6..00000000 --- a/redmine-net40-api/JSonConverters/DetailConverter.cs +++ /dev/null @@ -1,51 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class DetailConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var detail = new Detail(); - - detail.NewValue = dictionary.GetValue("new_value"); - detail.OldValue = dictionary.GetValue("old_value"); - detail.Property = dictionary.GetValue("property"); - detail.StatusId = dictionary.GetValue("name"); - - return detail; - } - - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) { return null; } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(Detail) }); } } - - #endregion - } -} diff --git a/redmine-net40-api/JSonConverters/ErrorConverter.cs b/redmine-net40-api/JSonConverters/ErrorConverter.cs deleted file mode 100644 index 8cf011a4..00000000 --- a/redmine-net40-api/JSonConverters/ErrorConverter.cs +++ /dev/null @@ -1,45 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ErrorConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var error = new Error { Info = dictionary.GetValue("error") }; - return error; - } - - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) { return null; } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(Error) }); } } - - #endregion - } -} diff --git a/redmine-net40-api/JSonConverters/GroupConverter.cs b/redmine-net40-api/JSonConverters/GroupConverter.cs deleted file mode 100644 index b39c93e6..00000000 --- a/redmine-net40-api/JSonConverters/GroupConverter.cs +++ /dev/null @@ -1,70 +0,0 @@ -/* - Copyright 2011 - 2015 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.Linq; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class GroupConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var group = new Group(); - - group.Id = dictionary.GetValue("id"); - group.Name = dictionary.GetValue("name"); - group.Users = dictionary.GetValueAsCollection("users"); - group.CustomFields = dictionary.GetValueAsCollection("custom_fields"); - group.Memberships = dictionary.GetValueAsCollection("memberships"); - - return group; - } - - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Group; - var root = new Dictionary(); - var result = new Dictionary(); - - if (entity != null) - { - result.Add("name", entity.Name); - if (entity.Users != null) - result.Add("user_ids", entity.Users.Select(x => x.Id).ToArray()); - - root["group"] = result; - return root; - } - - return result; - } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(Group) }); } } - - #endregion - } -} diff --git a/redmine-net40-api/JSonConverters/GroupUserConverter.cs b/redmine-net40-api/JSonConverters/GroupUserConverter.cs deleted file mode 100644 index 019243d2..00000000 --- a/redmine-net40-api/JSonConverters/GroupUserConverter.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class GroupUserConverter : IdentifiableNameConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var userGroup = new GroupUser(); - - userGroup.Id = dictionary.GetValue("id"); - userGroup.Name = dictionary.GetValue("name"); - - return userGroup; - } - - return null; - } - - 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 100644 index 9bffdc4a..00000000 --- a/redmine-net40-api/JSonConverters/IdentifiableNameConverter.cs +++ /dev/null @@ -1,62 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IdentifiableNameConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var entity = new IdentifiableName(); - - entity.Id = dictionary.GetValue("id"); - entity.Name = dictionary.GetValue("name"); - - return entity; - } - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as IdentifiableName; - var result = new Dictionary(); - - if (entity != null) - { - result.WriteIdIfNotNull(entity, "id"); - if (!string.IsNullOrEmpty(entity.Name)) - result.Add("name", entity.Name); - return result; - } - - return result; - } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(IdentifiableName) }); } } - - #endregion - } -} diff --git a/redmine-net40-api/JSonConverters/IssueCategoryConverter.cs b/redmine-net40-api/JSonConverters/IssueCategoryConverter.cs deleted file mode 100644 index fc83d50d..00000000 --- a/redmine-net40-api/JSonConverters/IssueCategoryConverter.cs +++ /dev/null @@ -1,68 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssueCategoryConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var issueCategory = new IssueCategory(); - - issueCategory.Id = dictionary.GetValue("id"); - issueCategory.Project = dictionary.GetValueAsIdentifiableName("project"); - issueCategory.AsignTo = dictionary.GetValueAsIdentifiableName("assigned_to"); - issueCategory.Name = dictionary.GetValue("name"); - - return issueCategory; - } - - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as IssueCategory; - var root = new Dictionary(); - var result = new Dictionary(); - - if (entity != null) - { - result.Add("name", entity.Name); - result.WriteIdIfNotNull(entity.Project, "project_id"); - result.WriteIdIfNotNull(entity.AsignTo, "assigned_to_id"); - - root["issue_category"] = result; - return root; - } - - return result; - } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(IssueCategory) }); } } - - #endregion - } -} diff --git a/redmine-net40-api/JSonConverters/IssueChildConverter.cs b/redmine-net40-api/JSonConverters/IssueChildConverter.cs deleted file mode 100644 index 03945a55..00000000 --- a/redmine-net40-api/JSonConverters/IssueChildConverter.cs +++ /dev/null @@ -1,47 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssueChildConverter : JavaScriptConverter - { - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var issueChild = new IssueChild - { - Id = dictionary.GetValue("id"), - Tracker = dictionary.GetValueAsIdentifiableName("tracker"), - Subject = dictionary.GetValue("subject") - }; - - return issueChild; - } - - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) { return null; } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(IssueChild) }); } } - } -} diff --git a/redmine-net40-api/JSonConverters/IssueConverter.cs b/redmine-net40-api/JSonConverters/IssueConverter.cs deleted file mode 100644 index 9f79bb27..00000000 --- a/redmine-net40-api/JSonConverters/IssueConverter.cs +++ /dev/null @@ -1,140 +0,0 @@ -/* - Copyright 2011 - 2015 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.Globalization; -using System.Linq; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssueConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var issue = new Issue(); - - issue.Id = dictionary.GetValue("id"); - issue.Description = dictionary.GetValue("description"); - issue.Project = dictionary.GetValueAsIdentifiableName("project"); - issue.Tracker = dictionary.GetValueAsIdentifiableName("tracker"); - issue.Status = dictionary.GetValueAsIdentifiableName("status"); - issue.CreatedOn = dictionary.GetValue("created_on"); - issue.UpdatedOn = dictionary.GetValue("updated_on"); - issue.ClosedOn = dictionary.GetValue("closed_on"); - issue.Priority = dictionary.GetValueAsIdentifiableName("priority"); - issue.Author = dictionary.GetValueAsIdentifiableName("author"); - issue.AssignedTo = dictionary.GetValueAsIdentifiableName("assigned_to"); - issue.Category = dictionary.GetValueAsIdentifiableName("category"); - issue.FixedVersion = dictionary.GetValueAsIdentifiableName("fixed_version"); - issue.Subject = dictionary.GetValue("subject"); - issue.Notes = dictionary.GetValue("notes"); - issue.IsPrivate = dictionary.GetValue("is_private"); - issue.StartDate = dictionary.GetValue("start_date"); - issue.DueDate = dictionary.GetValue("due_date"); - issue.DoneRatio = dictionary.GetValue("done_ratio"); - issue.EstimatedHours = dictionary.GetValue("estimated_hours"); - issue.ParentIssue = dictionary.GetValueAsIdentifiableName("parent"); - - issue.CustomFields = dictionary.GetValueAsCollection("custom_fields"); - issue.Attachments = dictionary.GetValueAsCollection("attachments"); - issue.Relations = dictionary.GetValueAsCollection("relations"); - issue.Journals = dictionary.GetValueAsCollection("journals"); - issue.Changesets = dictionary.GetValueAsCollection("changesets"); - issue.Watchers = dictionary.GetValueAsCollection("watchers"); - issue.Children = dictionary.GetValueAsCollection("children"); - return issue; - } - - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Issue; - var root = new Dictionary(); - var result = new Dictionary(); - - if (entity != null) - { - result.Add("subject", entity.Subject); - result.Add("description", entity.Description); - result.Add("notes", entity.Notes); - if (entity.Id != 0) - { - result.Add("private_notes", entity.IsPrivate); - } - result.Add("is_private", entity.IsPrivate); - result.WriteIdIfNotNull(entity.Project, "project_id"); - result.WriteIdIfNotNull(entity.Priority, "priority_id"); - result.WriteIdIfNotNull(entity.Status, "status_id"); - result.WriteIdIfNotNull(entity.Category, "category_id"); - result.WriteIdIfNotNull(entity.Tracker, "tracker_id"); - result.WriteIdIfNotNull(entity.AssignedTo, "assigned_to_id"); - result.WriteIdIfNotNull(entity.FixedVersion, "fixed_version_id"); - // result.WriteIdIfNotNull(entity.ParentIssue, "parent_issue_id"); - result.WriteIfNotDefaultOrNull(entity.EstimatedHours, "estimated_hours"); - - if(entity.ParentIssue == null) - result.Add("parent_issue_id", null); - else - { - result.Add("parent_issue_id", entity.ParentIssue.Id); - } - if (entity.StartDate != null) - result.Add("start_date", entity.StartDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); - - if (entity.DueDate != null) - result.Add("due_date", entity.DueDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); - - if (entity.UpdatedOn != null) - result.Add("updated_on", entity.UpdatedOn.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); - - if(entity.DoneRatio != null) - result.Add("done_ratio", entity.DoneRatio); - - if (entity.Uploads != null) result.Add("uploads", entity.Uploads.ToArray()); - - if (entity.CustomFields != null) - { - serializer.RegisterConverters(new[] { new IssueCustomFieldConverter() }); - result.Add("custom_fields", entity.CustomFields.ToArray()); - } - - if (entity.Watchers != null) - { - serializer.RegisterConverters(new[] { new WatcherConverter() }); - result.Add("watcher_user_ids", entity.Watchers.ToArray()); - } - - root["issue"] = result; - return root; - } - - return result; - } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(Issue) }); } } - - #endregion - } -} diff --git a/redmine-net40-api/JSonConverters/IssueCustomFieldConverter.cs b/redmine-net40-api/JSonConverters/IssueCustomFieldConverter.cs deleted file mode 100644 index 54622968..00000000 --- a/redmine-net40-api/JSonConverters/IssueCustomFieldConverter.cs +++ /dev/null @@ -1,91 +0,0 @@ -/* - Copyright 2011 - 2015 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; -using System.Collections.Generic; -using System.Linq; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssueCustomFieldConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var customField = new IssueCustomField(); - - customField.Id = dictionary.GetValue("id"); - customField.Name = dictionary.GetValue("name"); - customField.Multiple = dictionary.GetValue("multiple"); - - var val = dictionary.GetValue("value"); - - if (val != null) - { - 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; - } - - return null; - } - - 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("id", entity.Id); - if (itemsCount > 1) - { - result.Add("value", entity.Values.Select(x => x.Info).ToArray()); - } - else - { - result.Add("value", itemsCount > 0 ? entity.Values[0].Info : null); - } - - return result; - } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(IssueCustomField) }); } } - - #endregion - } -} diff --git a/redmine-net40-api/JSonConverters/IssuePriorityConverter.cs b/redmine-net40-api/JSonConverters/IssuePriorityConverter.cs deleted file mode 100644 index 3d675b40..00000000 --- a/redmine-net40-api/JSonConverters/IssuePriorityConverter.cs +++ /dev/null @@ -1,46 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssuePriorityConverter : JavaScriptConverter - { - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var issuePriority = new IssuePriority(); - - issuePriority.Id = dictionary.GetValue("id"); - issuePriority.Name = dictionary.GetValue("name"); - issuePriority.IsDefault = dictionary.GetValue("is_default"); - - return issuePriority; - } - - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) { return null; } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(IssuePriority) }); } } - } -} diff --git a/redmine-net40-api/JSonConverters/IssueRelationConverter.cs b/redmine-net40-api/JSonConverters/IssueRelationConverter.cs deleted file mode 100644 index b43ef9a3..00000000 --- a/redmine-net40-api/JSonConverters/IssueRelationConverter.cs +++ /dev/null @@ -1,70 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssueRelationConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var issueRelation = new IssueRelation(); - - issueRelation.Id = dictionary.GetValue("id"); - issueRelation.IssueId = dictionary.GetValue("issue_id"); - issueRelation.IssueToId = dictionary.GetValue("issue_to_id"); - issueRelation.Type = dictionary.GetValue("relation_type"); - issueRelation.Delay = dictionary.GetValue("delay"); - - return issueRelation; - } - - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as IssueRelation; - var root = new Dictionary(); - var result = new Dictionary(); - - if (entity != null) - { - result.Add("issue_to_id", entity.IssueToId); - result.Add("relation_type", entity.Type.ToString()); - if (entity.Type == IssueRelationType.precedes || entity.Type == IssueRelationType.follows) - result.WriteIfNotDefaultOrNull(entity.Delay, "delay"); - - root["relation"] = result; - return root; - } - - return result; - } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(IssueRelation) }); } } - - #endregion - } -} diff --git a/redmine-net40-api/JSonConverters/IssueStatusConverter.cs b/redmine-net40-api/JSonConverters/IssueStatusConverter.cs deleted file mode 100644 index 7e9743bd..00000000 --- a/redmine-net40-api/JSonConverters/IssueStatusConverter.cs +++ /dev/null @@ -1,50 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssueStatusConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var issueStatus = new IssueStatus(); - - issueStatus.Id = dictionary.GetValue("id"); - issueStatus.Name = dictionary.GetValue("name"); - issueStatus.IsClosed = dictionary.GetValue("is_closed"); - issueStatus.IsDefault = dictionary.GetValue("is_default"); - return issueStatus; - } - - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) { return null; } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(IssueStatus) }); } } - - #endregion - } -} diff --git a/redmine-net40-api/JSonConverters/JournalConverter.cs b/redmine-net40-api/JSonConverters/JournalConverter.cs deleted file mode 100644 index cc4a1cd1..00000000 --- a/redmine-net40-api/JSonConverters/JournalConverter.cs +++ /dev/null @@ -1,52 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class JournalConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var journal = new Journal(); - - journal.Id = dictionary.GetValue("id"); - journal.Notes = dictionary.GetValue("notes"); - journal.User = dictionary.GetValueAsIdentifiableName("user"); - journal.CreatedOn = dictionary.GetValue("created_on"); - journal.Details = dictionary.GetValueAsCollection("details"); - - return journal; - } - - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) { return null; } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(Journal) }); } } - - #endregion - } -} diff --git a/redmine-net40-api/JSonConverters/MembershipConverter.cs b/redmine-net40-api/JSonConverters/MembershipConverter.cs deleted file mode 100644 index c444ad47..00000000 --- a/redmine-net40-api/JSonConverters/MembershipConverter.cs +++ /dev/null @@ -1,50 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class MembershipConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var membership = new Membership(); - - membership.Id = dictionary.GetValue("id"); - membership.Project = dictionary.GetValueAsIdentifiableName("project"); - membership.Roles = dictionary.GetValueAsCollection("roles"); - - return membership; - } - - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) { return null; } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(Membership) }); } } - - #endregion - } -} diff --git a/redmine-net40-api/JSonConverters/MembershipRoleConverter.cs b/redmine-net40-api/JSonConverters/MembershipRoleConverter.cs deleted file mode 100644 index ec272891..00000000 --- a/redmine-net40-api/JSonConverters/MembershipRoleConverter.cs +++ /dev/null @@ -1,50 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class MembershipRoleConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var membershipRole = new MembershipRole(); - - membershipRole.Id = dictionary.GetValue("id"); - membershipRole.Inherited = dictionary.GetValue("inherited"); - membershipRole.Name = dictionary.GetValue("name"); - - return membershipRole; - } - - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) { return null; } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(MembershipRole) }); } } - - #endregion - } -} diff --git a/redmine-net40-api/JSonConverters/NewsConverter.cs b/redmine-net40-api/JSonConverters/NewsConverter.cs deleted file mode 100644 index 7eb3622b..00000000 --- a/redmine-net40-api/JSonConverters/NewsConverter.cs +++ /dev/null @@ -1,53 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class NewsConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var news = new News(); - - news.Id = dictionary.GetValue("id"); - news.Author = dictionary.GetValueAsIdentifiableName("author"); - news.CreatedOn = dictionary.GetValue("created_on"); - news.Description = dictionary.GetValue("description"); - news.Project = dictionary.GetValueAsIdentifiableName("project"); - news.Summary = dictionary.GetValue("summary"); - news.Title = dictionary.GetValue("title"); - - return news; - } - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) { return null; } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(News) }); } } - - #endregion - } -} diff --git a/redmine-net40-api/JSonConverters/PermissionConverter.cs b/redmine-net40-api/JSonConverters/PermissionConverter.cs deleted file mode 100644 index b509ad18..00000000 --- a/redmine-net40-api/JSonConverters/PermissionConverter.cs +++ /dev/null @@ -1,41 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class PermissionConverter : JavaScriptConverter - { - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var permission = new Permission { Info = dictionary.GetValue("permission") }; - return permission; - } - - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) { return null; } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(Permission) }); } } - } -} \ 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 0c2c8b9f..00000000 --- a/redmine-net40-api/JSonConverters/ProjectConverter.cs +++ /dev/null @@ -1,99 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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.Linq; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ProjectConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var project = new Project(); - - project.Id = dictionary.GetValue("id"); - project.Description = dictionary.GetValue("description"); - project.HomePage = dictionary.GetValue("homepage"); - project.Name = dictionary.GetValue("name"); - project.Identifier = dictionary.GetValue("identifier"); - project.Status = dictionary.GetValue("status"); - project.CreatedOn = dictionary.GetValue("created_on"); - project.UpdatedOn = dictionary.GetValue("updated_on"); - project.Trackers = dictionary.GetValueAsCollection("trackers"); - project.CustomFields = dictionary.GetValueAsCollection("custom_fields"); - project.IsPublic = dictionary.GetValue("is_public"); - project.Parent = dictionary.GetValueAsIdentifiableName("parent"); - project.IssueCategories = dictionary.GetValueAsCollection("issue_categories"); - project.EnabledModules = dictionary.GetValueAsCollection("enabled_modules"); - return project; - } - - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Project; - var root = new Dictionary(); - var result = new Dictionary(); - - if (entity != null) - { - result.Add("name", entity.Name); - result.Add("identifier", entity.Identifier); - result.Add("description", entity.Description); - result.Add("homepage", entity.HomePage); - result.Add("inherit_members", entity.InheritMembers); - result.Add("is_public", entity.IsPublic); - - if (entity.Parent != null) - result.Add("parent_id", entity.Parent.Id); - - if (entity.CustomFields != null) - { - serializer.RegisterConverters(new[] { new IssueCustomFieldConverter() }); - result.Add("custom_fields", entity.CustomFields.ToArray()); - } - - if (entity.EnabledModules != null) - { - var enabledModuleNames = entity.EnabledModules - .Where(projectEnabledModule => !string.IsNullOrEmpty(projectEnabledModule.Name)) - .Aggregate("", (current, projectEnabledModule) => current + projectEnabledModule.Name); - - result.Add("enabled_module_names", enabledModuleNames); - } - - root["project"] = result; - return root; - } - - return result; - } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(Project) }); } } - - #endregion - } -} diff --git a/redmine-net40-api/JSonConverters/ProjectEnabledModuleConverter.cs b/redmine-net40-api/JSonConverters/ProjectEnabledModuleConverter.cs deleted file mode 100644 index 05560df4..00000000 --- a/redmine-net40-api/JSonConverters/ProjectEnabledModuleConverter.cs +++ /dev/null @@ -1,49 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ProjectEnabledModuleConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if ((dictionary != null)) - { - var projectEnableModule = new ProjectEnabledModule(); - projectEnableModule.Id = dictionary.GetValue("id"); - projectEnableModule.Name = dictionary.GetValue("name"); - return projectEnableModule; - } - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) { return null; } - - 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 100644 index cddbf14d..00000000 --- a/redmine-net40-api/JSonConverters/ProjectIssueCategoryConverter.cs +++ /dev/null @@ -1,64 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ProjectIssueCategoryConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if ((dictionary != null)) - { - var projectTracker = new ProjectIssueCategory(); - projectTracker.Id = dictionary.GetValue("id"); - projectTracker.Name = dictionary.GetValue("name"); - return projectTracker; - } - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as ProjectIssueCategory; - var root = new Dictionary(); - var result = new Dictionary(); - - if (entity != null) - { - result.Add("id", entity.Id); - result.Add("name", entity.Name); - - root["issue_category"] = result; - return root; - } - return result; - } - - 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 100644 index cac2d410..00000000 --- a/redmine-net40-api/JSonConverters/ProjectMembershipConverter.cs +++ /dev/null @@ -1,68 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ProjectMembershipConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var projectMembership = new ProjectMembership(); - - projectMembership.Id = dictionary.GetValue("id"); - projectMembership.Group = dictionary.GetValueAsIdentifiableName("group"); - projectMembership.Project = dictionary.GetValueAsIdentifiableName("project"); - projectMembership.Roles = dictionary.GetValueAsCollection("roles"); - projectMembership.User = dictionary.GetValueAsIdentifiableName("user"); - - return projectMembership; - } - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as ProjectMembership; - var root = new Dictionary(); - var result = new Dictionary(); - - if (entity != null) - { - if (entity.User != null) - result.Add("user_id", entity.User.Id); - result.Add("role_ids", entity.Roles.ToArray()); - - root["membership"] = result; - return root; - } - - return result; - } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(ProjectMembership) }); } } - - #endregion - } -} diff --git a/redmine-net40-api/JSonConverters/ProjectTrackerConverter.cs b/redmine-net40-api/JSonConverters/ProjectTrackerConverter.cs deleted file mode 100644 index bfae9155..00000000 --- a/redmine-net40-api/JSonConverters/ProjectTrackerConverter.cs +++ /dev/null @@ -1,52 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ProjectTrackerConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if ((dictionary != null)) - { - var projectTracker = new ProjectTracker(); - projectTracker.Id = dictionary.GetValue("id"); - projectTracker.Name = dictionary.GetValue("name"); - return projectTracker; - } - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - public override IEnumerable SupportedTypes - { - get { return new List(new[] { typeof(ProjectTracker) }); } - } - - #endregion - } -} diff --git a/redmine-net40-api/JSonConverters/QueryConverter.cs b/redmine-net40-api/JSonConverters/QueryConverter.cs deleted file mode 100644 index 42005399..00000000 --- a/redmine-net40-api/JSonConverters/QueryConverter.cs +++ /dev/null @@ -1,51 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class QueryConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var query = new Query(); - - query.Id = dictionary.GetValue("id"); - query.IsPublic = dictionary.GetValue("is_public"); - query.ProjectId = dictionary.GetValue("project_id"); - query.Name = dictionary.GetValue("name"); - - return query; - } - - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) { return null; } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(Query) }); } } - - #endregion - } -} diff --git a/redmine-net40-api/JSonConverters/RoleConverter.cs b/redmine-net40-api/JSonConverters/RoleConverter.cs deleted file mode 100644 index 3e5083e4..00000000 --- a/redmine-net40-api/JSonConverters/RoleConverter.cs +++ /dev/null @@ -1,62 +0,0 @@ -/* - Copyright 2011 - 2015 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; -using System.Collections.Generic; -using System.Linq; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class RoleConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var role = new Role(); - - role.Id = dictionary.GetValue("id"); - role.Name = dictionary.GetValue("name"); - - var permissions = dictionary["permissions"] as ArrayList; - 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; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) { return null; } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(Role) }); } } - - #endregion - } -} diff --git a/redmine-net40-api/JSonConverters/TimeEntryActivityConverter.cs b/redmine-net40-api/JSonConverters/TimeEntryActivityConverter.cs deleted file mode 100644 index ec1bc4c1..00000000 --- a/redmine-net40-api/JSonConverters/TimeEntryActivityConverter.cs +++ /dev/null @@ -1,41 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class TimeEntryActivityConverter : JavaScriptConverter - { - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var timeEntryActivity = new TimeEntryActivity { Id = dictionary.GetValue("id"), Name = dictionary.GetValue("name"), IsDefault = dictionary.GetValue("is_default") }; - return timeEntryActivity; - } - - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) { return null; } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(TimeEntryActivity) }); } } - } -} diff --git a/redmine-net40-api/JSonConverters/TimeEntryConverter.cs b/redmine-net40-api/JSonConverters/TimeEntryConverter.cs deleted file mode 100644 index 524c6364..00000000 --- a/redmine-net40-api/JSonConverters/TimeEntryConverter.cs +++ /dev/null @@ -1,87 +0,0 @@ -/* - Copyright 2011 - 2015 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.Globalization; -using System.Linq; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class TimeEntryConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var timeEntry = new TimeEntry(); - - timeEntry.Id = dictionary.GetValue("id"); - timeEntry.Activity = dictionary.GetValueAsIdentifiableName(dictionary.ContainsKey("activity") ? "activity" : "activity_id"); - timeEntry.Comments = dictionary.GetValue("comments"); - timeEntry.Hours = dictionary.GetValue("hours"); - timeEntry.Issue = dictionary.GetValueAsIdentifiableName(dictionary.ContainsKey("issue") ? "issue" : "issue_id"); - timeEntry.Project = dictionary.GetValueAsIdentifiableName(dictionary.ContainsKey("project") ? "project" : "project_id"); - timeEntry.SpentOn = dictionary.GetValue("spent_on"); - timeEntry.User = dictionary.GetValueAsIdentifiableName("user"); - timeEntry.CustomFields = dictionary.GetValueAsCollection("custom_fields"); - timeEntry.CreatedOn = dictionary.GetValue("created_on"); - timeEntry.UpdatedOn = dictionary.GetValue("updated_on"); - - return timeEntry; - } - - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as TimeEntry; - var root = new Dictionary(); - var result = new Dictionary(); - - if (entity != null) - { - result.WriteIdIfNotNull(entity.Issue, "issue_id"); - result.WriteIdIfNotNull(entity.Project, "project_id"); - result.WriteIdIfNotNull(entity.Activity, "activity_id"); - if (!entity.SpentOn.HasValue) entity.SpentOn = DateTime.Now; - result.Add("spent_on", entity.SpentOn.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); - result.Add("hours", entity.Hours); - result.Add("comments", entity.Comments); - - if (entity.CustomFields != null) - { - serializer.RegisterConverters(new[] { new IssueCustomFieldConverter() }); - result.Add("custom_fields", entity.CustomFields.ToArray()); - } - - root["time_entry"] = result; - return root; - } - - return result; - } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(TimeEntry) }); } } - - #endregion - } -} diff --git a/redmine-net40-api/JSonConverters/TrackerConverter.cs b/redmine-net40-api/JSonConverters/TrackerConverter.cs deleted file mode 100644 index 7e5a5c8d..00000000 --- a/redmine-net40-api/JSonConverters/TrackerConverter.cs +++ /dev/null @@ -1,45 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class TrackerConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var tracker = new Tracker { Id = dictionary.GetValue("id"), Name = dictionary.GetValue("name") }; - return tracker; - } - - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) { return null; } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(Tracker) }); } } - - #endregion - } -} diff --git a/redmine-net40-api/JSonConverters/TrackerCustomFieldConverter.cs b/redmine-net40-api/JSonConverters/TrackerCustomFieldConverter.cs deleted file mode 100644 index e4a8044a..00000000 --- a/redmine-net40-api/JSonConverters/TrackerCustomFieldConverter.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class TrackerCustomFieldConverter : IdentifiableNameConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var entity = new TrackerCustomField(); - - entity.Id = dictionary.GetValue("id"); - entity.Name = dictionary.GetValue("name"); - - return entity; - } - - return null; - } - - 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 100644 index ed35f590..00000000 --- a/redmine-net40-api/JSonConverters/UploadConverter.cs +++ /dev/null @@ -1,67 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class UploadConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var upload = new Upload(); - - upload.ContentType = dictionary.GetValue("content_type"); - upload.FileName = dictionary.GetValue("filename"); - upload.Token = dictionary.GetValue("token"); - upload.Description = dictionary.GetValue("description"); - return upload; - } - - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Upload; - var root = new Dictionary(); - var result = new Dictionary(); - - if (entity != null) - { - result.Add("content_type", entity.ContentType); - result.Add("filename", entity.FileName); - result.Add("token", entity.Token); - result.Add("description", entity.Description); - root["upload"] = result; - return root; - } - - return result; - } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(Upload) }); } } - - #endregion - } -} diff --git a/redmine-net40-api/JSonConverters/UserConverter.cs b/redmine-net40-api/JSonConverters/UserConverter.cs deleted file mode 100644 index c82b7d60..00000000 --- a/redmine-net40-api/JSonConverters/UserConverter.cs +++ /dev/null @@ -1,85 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class UserConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - User user = new User(); - user.Login = dictionary.GetValue("login"); - user.Id = dictionary.GetValue("id"); - user.FirstName = dictionary.GetValue("firstname"); - user.LastName = dictionary.GetValue("lastname"); - user.Email = dictionary.GetValue("mail"); - user.AuthenticationModeId = dictionary.GetValue("auth_source_id"); - user.CreatedOn = dictionary.GetValue("created_on"); - user.LastLoginOn = dictionary.GetValue("last_login_on"); - user.ApiKey = dictionary.GetValue("api_key"); - user.Status = dictionary.GetValue("status"); - user.MustChangePassword = dictionary.GetValue("must_change_passwd"); - user.CustomFields = dictionary.GetValueAsCollection("custom_fields"); - user.Memberships = dictionary.GetValueAsCollection("memberships"); - user.Groups = dictionary.GetValueAsCollection("groups"); - - return user; - } - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as User; - var root = new Dictionary(); - var result = new Dictionary(); - - if (entity != null) - { - result.Add("login", entity.Login); - result.Add("firstname", entity.FirstName); - result.Add("lastname", entity.LastName); - result.Add("mail", entity.Email); - result.Add("password", entity.Password); - result.Add("must_change_passwd", entity.MustChangePassword); - result.WriteIfNotDefaultOrNull(entity.AuthenticationModeId, "auth_source_id"); - - if (entity.CustomFields != null) - { - serializer.RegisterConverters(new[] { new IssueCustomFieldConverter() }); - result.Add("custom_fields", entity.CustomFields.ToArray()); - } - - root["user"] = result; - return root; - } - return result; - } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(User) }); } } - - #endregion - } -} diff --git a/redmine-net40-api/JSonConverters/UserGroupConverter.cs b/redmine-net40-api/JSonConverters/UserGroupConverter.cs deleted file mode 100644 index 4d6eae6e..00000000 --- a/redmine-net40-api/JSonConverters/UserGroupConverter.cs +++ /dev/null @@ -1,46 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class UserGroupConverter : IdentifiableNameConverter - { - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var userGroup = new UserGroup(); - - userGroup.Id = dictionary.GetValue("id"); - userGroup.Name = dictionary.GetValue("name"); - - return userGroup; - } - - return null; - } - #region Overrides of JavaScriptConverter - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(UserGroup) }); } } - - #endregion - } -} diff --git a/redmine-net40-api/JSonConverters/VersionConverter.cs b/redmine-net40-api/JSonConverters/VersionConverter.cs deleted file mode 100644 index dff29195..00000000 --- a/redmine-net40-api/JSonConverters/VersionConverter.cs +++ /dev/null @@ -1,79 +0,0 @@ -/* - Copyright 2011 - 2015 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.Globalization; -using System.Web.Script.Serialization; -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 - - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var version = new Version(); - - version.Id = dictionary.GetValue("id"); - version.Description = dictionary.GetValue("description"); - version.Name = dictionary.GetValue("name"); - version.CreatedOn = dictionary.GetValue("created_on"); - version.UpdatedOn = dictionary.GetValue("updated_on"); - version.DueDate = dictionary.GetValue("due_date"); - version.Project = dictionary.GetValueAsIdentifiableName("project"); - version.Sharing = dictionary.GetValue("sharing"); - version.Status = dictionary.GetValue("status"); - version.CustomFields = dictionary.GetValueAsCollection("custom_fields"); - - return version; - } - - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Version; - var root = new Dictionary(); - var result = new Dictionary(); - - if (entity != null) - { - result.Add("name", entity.Name); - result.Add("status", entity.Status.ToString()); - result.Add("sharing", entity.Sharing.ToString()); - result.Add("description", entity.Description); - if (entity.DueDate != null) - result.Add("due_date", entity.DueDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); - - root["version"] = result; - return root; - } - - return result; - } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(Version) }); } } - - #endregion - } -} diff --git a/redmine-net40-api/JSonConverters/WatcherConverter.cs b/redmine-net40-api/JSonConverters/WatcherConverter.cs deleted file mode 100644 index 9c436bc0..00000000 --- a/redmine-net40-api/JSonConverters/WatcherConverter.cs +++ /dev/null @@ -1,59 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class WatcherConverter : JavaScriptConverter - { - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var watcher = new Watcher(); - - watcher.Id = dictionary.GetValue("id"); - watcher.Name = dictionary.GetValue("name"); - - return watcher; - } - - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Watcher; - var result = new Dictionary(); - - if (entity != null) - { - result.Add("id", entity.Id); - } - - return result; - } - - public override IEnumerable SupportedTypes - { - get { return new List(new[] { typeof(Watcher) }); } - } - } -} \ 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 cf243774..00000000 --- a/redmine-net40-api/JSonConverters/WikiPageConverter.cs +++ /dev/null @@ -1,69 +0,0 @@ -/* - Copyright 2011 - 2015 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.Web.Script.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class WikiPageConverter : JavaScriptConverter - { - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var tracker = new WikiPage(); - - tracker.Id = dictionary.GetValue("id"); - tracker.Author = dictionary.GetValueAsIdentifiableName("author"); - tracker.Comments = dictionary.GetValue("comments"); - tracker.CreatedOn = dictionary.GetValue("created_on"); - tracker.Text = dictionary.GetValue("text"); - tracker.Title = dictionary.GetValue("title"); - tracker.UpdatedOn = dictionary.GetValue("updated_on"); - tracker.Version = dictionary.GetValue("version"); - tracker.Attachments = dictionary.GetValueAsCollection("attachments"); - - return tracker; - } - - return null; - } - - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as WikiPage; - var root = new Dictionary(); - var result = new Dictionary(); - - if (entity != null) - { - result.Add("text", entity.Text); - result.Add("comments", entity.Comments); - result.WriteIfNotDefaultOrNull(entity.Version, "version"); - - root["wiki_page"] = result; - return root; - } - - return result; - } - - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(WikiPage) }); } } - } -} diff --git a/redmine-net40-api/MimeFormat.cs b/redmine-net40-api/MimeFormat.cs deleted file mode 100644 index 8dfd27f4..00000000 --- a/redmine-net40-api/MimeFormat.cs +++ /dev/null @@ -1,25 +0,0 @@ -ο»Ώ/* -Copyright 2011 - 2015 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. -*/ - - -namespace Redmine.Net.Api -{ - public enum MimeFormat - { - xml, - 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 2bf41ad6..00000000 --- a/redmine-net40-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-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 - 2015")] -[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.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/redmine-net40-api/RedmineException.cs b/redmine-net40-api/RedmineException.cs deleted file mode 100644 index 71013711..00000000 --- a/redmine-net40-api/RedmineException.cs +++ /dev/null @@ -1,42 +0,0 @@ -/* - Copyright 2011 - 2015 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.Runtime.Serialization; - -namespace Redmine.Net.Api -{ - public class RedmineException : Exception - { - public RedmineException() - : base() { } - - public RedmineException(string message) - : base(message) { } - - public RedmineException(string format, params object[] args) - : base(string.Format(format, args)) { } - - public RedmineException(string message, Exception innerException) - : base(message, innerException) { } - - public RedmineException(string format, Exception innerException, params object[] args) - : base(string.Format(format, args), innerException) { } - - protected RedmineException(SerializationInfo info, StreamingContext context) - : base(info, context) { } - } -} \ No newline at end of file diff --git a/redmine-net40-api/RedmineManager.cs b/redmine-net40-api/RedmineManager.cs deleted file mode 100644 index c4a4b090..00000000 --- a/redmine-net40-api/RedmineManager.cs +++ /dev/null @@ -1,839 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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.Collections.Specialized; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Text.RegularExpressions; -using System.Xml; -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 partial class RedmineManager - { - private const string REQUEST_FORMAT = "{0}/{1}/{2}.{3}"; - private const string FORMAT = "{0}/{1}.{2}"; - - private const string WIKI_INDEX_FORMAT = "{0}/projects/{1}/wiki/index.{2}"; - private const string WIKI_PAGE_FORMAT = "{0}/projects/{1}/wiki/{2}.{3}"; - private const string WIKI_VERSION_FORMAT = "{0}/projects/{1}/wiki/{2}/{3}.{4}"; - - private const string ENTITY_WITH_PARENT_FORMAT = "{0}/{1}/{2}/{3}.{4}"; - - private const string CURRENT_USER_URI = "current"; - private const string PUT = "PUT"; - private const string POST = "POST"; - private const string DELETE = "DELETE"; - - private readonly Dictionary urls = 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 host, apiKey, basicAuthorization; - private readonly MimeFormat mimeFormat; - private readonly CredentialCache credentialCache; - - /// - /// 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. - /// - 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. - /// - public string ImpersonateUser { get; set; } - - /// - /// Initializes a new instance of the class. - /// - /// The host. - /// - /// if set to true [verify server cert]. - public RedmineManager(string host, MimeFormat mimeFormat = MimeFormat.xml, bool verifyServerCert = true) - { - PageSize = 25; - - 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!"); - - this.host = host; - this.mimeFormat = mimeFormat; - - 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]. - public RedmineManager(string host, string apiKey, MimeFormat mimeFormat = MimeFormat.xml, bool verifyServerCert = true) - : this(host, mimeFormat, verifyServerCert) - { - PageSize = 25; - this.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]. - public RedmineManager(string host, string login, string password, MimeFormat mimeFormat = MimeFormat.xml, bool verifyServerCert = true) - : this(host, mimeFormat, verifyServerCert) - { - PageSize = 25; - 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!"); - - credentialCache = new CredentialCache { { uriResult, "Basic", new NetworkCredential(login, password) } }; - basicAuthorization = "Basic " + Convert.ToBase64String(Encoding.ASCII.GetBytes(login + ":" + password)); - } - - /// - /// 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) - { - return ExecuteDownload(string.Format(REQUEST_FORMAT, host, urls[typeof(User)], CURRENT_USER_URI, mimeFormat), "GetCurrentUser", parameters); - } - - /// - /// Returns a list of users. - /// - /// get only users with the given status. Default is 1 (active users) - /// filter users on their login, firstname, lastname and mail ; if the pattern contains a space, it will also return users whose firstname match the first word or lastname match the second word. - /// get only users who are members of the given group - /// - public IList GetUsers(UserStatus userStatus = UserStatus.STATUS_ACTIVE, string name = null, int groupId = 0) - { - var filters = new NameValueCollection { { "status", ((int)userStatus).ToString(CultureInfo.InvariantCulture) } }; - - if (!string.IsNullOrWhiteSpace(name)) filters.Add("name", name); - - if (groupId > 0) filters.Add("groupId", groupId.ToString(CultureInfo.InvariantCulture)); - - return GetTotalObjectList(filters); - } - - public void AddWatcher(int issueId, int userId) - { - ExecuteUpload(string.Format(REQUEST_FORMAT, host, urls[typeof(Issue)], issueId + "/watchers", mimeFormat), POST, mimeFormat == MimeFormat.xml - ? "" + userId + "" - : "{\"user_id\":\"" + userId + "\"}", "AddWatcher"); - } - - public void RemoveWatcher(int issueId, int userId) - { - ExecuteUpload(string.Format(REQUEST_FORMAT, host, urls[typeof(Issue)], issueId + "/watchers/" + userId, mimeFormat), DELETE, string.Empty, "RemoveWatcher"); - } - - /// - /// Adds an existing user to a group. - /// - /// The group id. - /// The user id. - public void AddUser(int groupId, int userId) - { - ExecuteUpload(string.Format(REQUEST_FORMAT, host, urls[typeof(Group)], groupId + "/users", mimeFormat), POST, mimeFormat == MimeFormat.xml - ? "" + userId + "" - : "{\"user_id\":\"" + userId + "\"}", "AddUser"); - } - - /// - /// Removes an user from a group. - /// - /// The group id. - /// The user id. - public void DeleteUser(int groupId, int userId) - { - ExecuteUpload(string.Format(REQUEST_FORMAT, host, urls[typeof(Group)], groupId + "/users/" + userId, mimeFormat), DELETE, string.Empty, "DeleteUser"); - } - - /// - /// Downloads the user whose credentials are used to access the API. This method does not block the calling thread. - /// - /// Returns the Guid associated with the async request. - /// An error occurred during deserialization. The original exception is available - /// using the System.Exception.InnerException property. - /// - /// Returns the details of a wiki page or the details of an old version of a wiki page if the version parameter is set. - /// - /// The project id or identifier. - /// - /// attachments - /// The accepted parameters are: memberships and groups (added in 2.1). - /// - /// The wiki page name. - /// The version of the wiki page. - /// - public WikiPage GetWikiPage(string projectId, NameValueCollection parameters, string pageName, uint version = 0) - { - string address = version == 0 - ? string.Format(WIKI_PAGE_FORMAT, host, projectId, pageName, mimeFormat) - : string.Format(WIKI_VERSION_FORMAT, host, projectId, pageName, version, mimeFormat); - - return ExecuteDownload(address, "GetWikiPage", parameters); - } - - /// - /// Returns the list of all pages in a project wiki. - /// - /// The project id or identifier. - /// - public IList GetAllWikiPages(string projectId) - { - int totalCount; - return ExecuteDownloadList(string.Format(WIKI_INDEX_FORMAT, host, projectId, mimeFormat), "GetAllWikiPages", "wiki_pages", out totalCount); - } - - /// - /// 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) - { - string result = Serialize(wikiPage); - - if (string.IsNullOrEmpty(result)) return null; - - return ExecuteUpload(string.Format(WIKI_PAGE_FORMAT, host, projectId, pageName, mimeFormat), PUT, result, "CreateOrUpdateWikiPage"); - } - - /// - /// 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) - { - ExecuteUpload(string.Format(WIKI_PAGE_FORMAT, host, projectId, pageName, mimeFormat), DELETE, string.Empty, "DeleteWikiPage"); - } - - /// - /// 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) - { - using (var wc = CreateUploadWebClient()) - { - try - { - var response = wc.UploadData(string.Format(FORMAT, host, "uploads", mimeFormat), data); - var responseString = Encoding.ASCII.GetString(response); - return Deserialize(responseString); - } - catch (WebException webException) - { - HandleWebException(webException, "Upload"); - } - } - - return null; - } - - public byte[] DownloadFile(string address) - { - using (var wc = CreateUploadWebClient()) - { - try - { - return wc.DownloadData(address); - } - catch (WebException webException) - { - HandleWebException(webException, "Download"); - } - } - - return null; - } - - /// - /// Returns a paginated list of objects. - /// - /// The type of objects to retrieve. - /// Optional filters and/or optional fetched data. - /// Returns a paginated list of objects. - /// By default only 25 results can be retrieved by 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. - /// - public IList GetObjectList(NameValueCollection parameters) where T : class, new() - { - int totalCount; - return GetObjectList(parameters, out totalCount); - } - - /// - /// Returns a paginated list of objects. - /// - /// The type of objects to retrieve. - /// Optional filters and/or optional fetched data. - /// Provide information about the total object count available in Redmine. - /// Returns a paginated list of objects. - /// By default only 25 results can be retrieved by 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. - /// - /// - public IList GetObjectList(NameValueCollection parameters, out int totalCount) where T : class, new() - { - totalCount = -1; - if (!urls.ContainsKey(typeof(T))) return null; - - var type = typeof(T); - string address = string.Empty; - - if (type == typeof(Version) || type == typeof(IssueCategory) || type == typeof(ProjectMembership)) - { - var projectId = GetOwnerId(parameters, "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."); - - address = string.Format(ENTITY_WITH_PARENT_FORMAT, host, "projects", projectId, urls[type], mimeFormat); - } - else - { - if (type == typeof(IssueRelation)) - { - string issueId = GetOwnerId(parameters, "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"); - - address = string.Format(ENTITY_WITH_PARENT_FORMAT, host, "issues", issueId, urls[type], mimeFormat); - } - else - { - if (type == typeof(News)) - { - var projectId = GetOwnerId(parameters, "project_id"); - if (!string.IsNullOrEmpty(projectId)) - { - address = string.Format(ENTITY_WITH_PARENT_FORMAT, host, "projects", projectId, urls[type], mimeFormat); - } - } - if (string.IsNullOrWhiteSpace(address)) - address = string.Format(FORMAT, host, urls[type], mimeFormat); - } - } - - return ExecuteDownloadList(address, "GetObjectList<" + type.Name + ">", urls[type], out totalCount, parameters); - } - - /// - /// 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. - /// 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. - /// - public IList GetTotalObjectList(NameValueCollection parameters) where T : class, new() - { - int totalCount, pageSize; - List resultList = null; - if (parameters == null) parameters = new NameValueCollection(); - int offset = 0; - int.TryParse(parameters["limit"], out pageSize); - if (pageSize == default(int)) - { - pageSize = PageSize > 0 ? PageSize : 25; - parameters.Set("limit", pageSize.ToString(CultureInfo.InvariantCulture)); - } - do - { - parameters.Set("offset", offset.ToString(CultureInfo.InvariantCulture)); - var tempResult = (List)GetObjectList(parameters, out totalCount); - if (resultList == null) - resultList = tempResult; - else - resultList.AddRange(tempResult); - offset += pageSize; - } - while (offset < totalCount); - return resultList; - } - - /// - /// Returns a Redmine object. - /// - /// 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 type = typeof(T); - - return !urls.ContainsKey(type) ? null : ExecuteDownload(string.Format(REQUEST_FORMAT, host, urls[type], id, mimeFormat), "GetObject<" + type.Name + ">", parameters); - } - - /// - /// 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. - /// - /// - /// - /// 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 = null) where T : class, new() - { - var type = typeof(T); - - if (!urls.ContainsKey(type)) return null; - - var result = Serialize(obj); - - if (string.IsNullOrEmpty(result)) return null; - - string address; - - if (type == typeof(Version) || type == typeof(IssueCategory) || type == typeof(ProjectMembership)) - { - if (string.IsNullOrEmpty(ownerId)) throw new RedmineException("The owner id(project id) is mandatory!"); - address = string.Format(ENTITY_WITH_PARENT_FORMAT, host, "projects", ownerId, urls[type], mimeFormat); - } - else - if (type == typeof(IssueRelation)) - { - if (string.IsNullOrEmpty(ownerId)) throw new RedmineException("The owner id(issue id) is mandatory!"); - address = string.Format(ENTITY_WITH_PARENT_FORMAT, host, "issues", ownerId, urls[type], mimeFormat); - } - else - address = string.Format(FORMAT, host, urls[type], mimeFormat); - - return ExecuteUpload(address, POST, result, "CreateObject<" + type.Name + ">"); - } - - /// - /// 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 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. - /// - /// When trying to update an object with invalid or missing attribute parameters, you will get a 422 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 type = typeof(T); - - if (!urls.ContainsKey(type)) return; - - var request = Serialize(obj); - if (string.IsNullOrEmpty(request)) return; - - request = Regex.Replace(request, @"\r\n|\r|\n", "\r\n", RegexOptions.Compiled); - - string address = string.Format(REQUEST_FORMAT, host, urls[type], id, mimeFormat); - - ExecuteUpload(address, PUT, request, "UpdateObject<" + type.Name + ">"); - } - - /// - /// Deletes the Redmine object. - /// - /// The type of objects to delete. - /// The id of the object to delete - /// Optional filters and/or optional fetched data. - /// - /// - public void DeleteObject(string id, NameValueCollection parameters) where T : class - { - var type = typeof(T); - - if (!urls.ContainsKey(typeof(T))) return; - - ExecuteUpload(string.Format(REQUEST_FORMAT, host, urls[type], id, mimeFormat), DELETE, string.Empty, "DeleteObject<" + type.Name + ">"); - } - - /// - /// Creates the Redmine web client. - /// - /// The parameters. - /// - /// - protected WebClient CreateWebClient(NameValueCollection parameters) - { - var webClient = new RedmineWebClient(); - - if (parameters != null) webClient.QueryString = parameters; - - if (!string.IsNullOrEmpty(apiKey)) - { - webClient.QueryString["key"] = apiKey; - } - else - { - if (credentialCache != null) webClient.Credentials = credentialCache; - } - - if (!string.IsNullOrWhiteSpace(ImpersonateUser)) webClient.Headers.Add("X-Redmine-Switch-User", ImpersonateUser); - - webClient.UseDefaultCredentials = false; - - webClient.Headers.Add("Content-Type", mimeFormat == MimeFormat.json ? "application/json; charset=utf-8" : "application/xml; charset=utf-8"); - webClient.Encoding = Encoding.UTF8; - webClient.Headers.Add("Authorization", basicAuthorization); - - return webClient; - } - - /// - /// Creates the Redmine web client. - /// - /// The parameters. - /// - /// - protected WebClient CreateUploadWebClient(NameValueCollection parameters = null) - { - var webClient = new RedmineWebClient(); - - if (parameters != null) webClient.QueryString = parameters; - - if (!string.IsNullOrEmpty(apiKey)) - { - webClient.QueryString["key"] = apiKey; - } - else - { - if (credentialCache != null) webClient.Credentials = credentialCache; - } - - webClient.UseDefaultCredentials = false; - - webClient.Headers.Add("Content-Type", "application/octet-stream"); - // Workaround - it seems that WebClient doesn't send credentials in each POST request - webClient.Headers.Add("Authorization", basicAuthorization); - - return webClient; - } - - /// - /// This is to take care of SSL certification validation which are not issued by Trusted Root CA. Recommended for testing only. - /// - /// The sender. - /// The cert. - /// The chain. - /// The error. - /// - /// - protected bool RemoteCertValidate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors error) - { - //Cert Validation Logic - return true; - } - - private void HandleWebException(WebException exception, string method) - { - if (exception == null) return; - - switch (exception.Status) - { - case WebExceptionStatus.Timeout: throw new RedmineException("Timeout!"); - case WebExceptionStatus.NameResolutionFailure: throw new RedmineException("Bad domain name!"); - case WebExceptionStatus.ProtocolError: - { - var response = (HttpWebResponse)exception.Response; - switch ((int)response.StatusCode) - { - case (int)HttpStatusCode.InternalServerError: - case (int)HttpStatusCode.Unauthorized: - case (int)HttpStatusCode.NotFound: - case (int)HttpStatusCode.Forbidden: - throw new RedmineException(response.StatusDescription); - - case (int)HttpStatusCode.Conflict: - throw new RedmineException("The page that you are trying to update is staled!"); - - case 422: - var errors = ReadWebExceptionResponse(exception.Response); - 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); - - case (int)HttpStatusCode.NotAcceptable: throw new RedmineException(response.StatusDescription); - } - } - break; - - default: throw new RedmineException(exception.Message); - } - } - - private static string GetOwnerId(NameValueCollection parameters, string parameterName) - { - if (parameters == null) return null; - string ownerId = parameters.Get(parameterName); - return string.IsNullOrEmpty(ownerId) ? null : ownerId; - } - - private IEnumerable ReadWebExceptionResponse(WebResponse webResponse) - { - using (var dataStream = webResponse.GetResponseStream()) - { - if (dataStream == null) return null; - var reader = new StreamReader(dataStream); - - var responseFromServer = reader.ReadToEnd(); - - if (responseFromServer.Trim().Length > 0) - { - try - { - int totalCount; - return DeserializeList(responseFromServer, "errors", out totalCount); - } - catch (Exception ex) - { - Trace.TraceError(ex.Message); - } - } - return null; - } - } - - private string Serialize(T obj) where T : class, new() - { - if (mimeFormat == MimeFormat.json) - return RedmineSerialization.JsonSerializer(obj); - return RedmineSerialization.ToXML(obj); - } - - private T Deserialize(string response) where T : class, new() - { - Type type = typeof(T); - - if (mimeFormat == MimeFormat.json) - { - var jsonRoot = (string)null; - if (type == typeof(IssueCategory)) jsonRoot = "issue_category"; - if (type == typeof(IssueRelation)) jsonRoot = "relation"; - if (type == typeof(TimeEntry)) jsonRoot = "time_entry"; - if (type == typeof(WikiPage)) jsonRoot = "wiki_page"; - - return RedmineSerialization.JsonDeserialize(response, jsonRoot); - } - - return RedmineSerialization.FromXML(response); - } - - private IList DeserializeList(string response, string jsonRoot, out int totalCount) where T : class, new() - { - Type type = typeof(T); - if (mimeFormat == MimeFormat.json) - { - if (type == typeof(IssuePriority)) jsonRoot = "issue_priorities"; - if (type == typeof(TimeEntryActivity)) jsonRoot = "time_entry_activities"; - return RedmineSerialization.JsonDeserializeToList(response, jsonRoot, out totalCount); - } - - using (var text = new StringReader(response)) - { - using (var xmlReader = new XmlTextReader(text)) - { - xmlReader.WhitespaceHandling = WhitespaceHandling.None; - xmlReader.Read(); - xmlReader.Read(); - - totalCount = xmlReader.ReadAttributeAsInt("total_count"); - - return xmlReader.ReadElementContentAsCollection(); - } - } - } - - private void ExecuteUpload(string address, string actionType, string data, string methodName) - { - using (var wc = CreateWebClient(null)) - { - try - { - if (actionType == POST || actionType == DELETE || actionType == PUT) - { - wc.UploadString(address, actionType, data); - } - } - catch (WebException webException) - { - HandleWebException(webException, methodName); - } - } - } - - private T ExecuteUpload(string address, string actionType, string data, string methodName) where T : class, new() - { - using (var wc = CreateWebClient(null)) - { - try - { - if (actionType == POST || actionType == DELETE || actionType == PUT) - { - var response = wc.UploadString(address, actionType, data); - if (!string.IsNullOrWhiteSpace(response)) - return Deserialize(response); - } - } - catch (WebException webException) - { - HandleWebException(webException, methodName); - } - return default(T); - } - } - - private T ExecuteDownload(string address, string methodName, NameValueCollection parameters = null) where T : class, new() - { - using (var wc = CreateWebClient(parameters)) - { - try - { - var response = wc.DownloadString(address); - return Deserialize(response); - } - catch (WebException webException) - { - HandleWebException(webException, methodName); - } - return default(T); - } - } - - private IList ExecuteDownloadList(string address, string methodName, string jsonRoot, out int totalCount, NameValueCollection parameters = null) where T : class, new() - { - totalCount = -1; - using (var wc = CreateWebClient(parameters)) - { - try - { - var response = wc.DownloadString(address); - return DeserializeList(response, jsonRoot, out totalCount); - } - catch (WebException webException) - { - HandleWebException(webException, methodName); - } - return null; - } - } - } -} \ No newline at end of file diff --git a/redmine-net40-api/RedmineManagerAsync.cs b/redmine-net40-api/RedmineManagerAsync.cs deleted file mode 100644 index e46e2fc0..00000000 --- a/redmine-net40-api/RedmineManagerAsync.cs +++ /dev/null @@ -1,379 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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.Specialized; -using System.IO; -using System.Net; -using System.Text; -using System.Threading; -using System.Xml; -using Redmine.Net.Api.Types; -using Version = Redmine.Net.Api.Types.Version; - -namespace Redmine.Net.Api -{ - public partial class RedmineManager - { - public event EventHandler DownloadCompleted; - - public Guid GetCurrentUserAsync(NameValueCollection parameters = null) - { - using (var wc = CreateWebClient(parameters)) - { - var id = Guid.NewGuid(); - wc.DownloadStringCompleted += WcDownloadStringCompleted; - wc.DownloadStringAsync(new Uri(string.Format(REQUEST_FORMAT, host, urls[typeof(User)], CURRENT_USER_URI, mimeFormat)), new AsyncToken { Method = RedmineMethod.GetCurrentUser, ResponseType = typeof(User), TokenId = id }); - return id; - } - } - - public Guid GetWikiPageAsync(string projectId, NameValueCollection parameters, string pageName, uint version = 0) - { - using (var wc = CreateWebClient(parameters)) - { - var id = Guid.NewGuid(); - wc.DownloadStringCompleted += WcDownloadStringCompleted; - wc.DownloadStringAsync(version == 0 - ? new Uri(string.Format(WIKI_PAGE_FORMAT, host, projectId, pageName, mimeFormat)) - : new Uri(string.Format(WIKI_VERSION_FORMAT, host, projectId, pageName, version, mimeFormat)), new AsyncToken { Method = RedmineMethod.GetWikiPage, Parameter = projectId, ResponseType = typeof(WikiPage), TokenId = id }); - - return id; - } - } - - public Guid CreateOrUpdateWikiPageAsync(string projectId, string pageName, WikiPage wikiPage) - { - var result = Serialize(wikiPage); - - if (string.IsNullOrEmpty(result)) return Guid.Empty; - - var id = Guid.NewGuid(); - using (var wc = CreateWebClient(null)) - { - wc.DownloadStringCompleted += WcDownloadStringCompleted; - wc.UploadStringAsync(new Uri(string.Format(WIKI_PAGE_FORMAT, host, projectId, pageName, mimeFormat)), PUT, result, new AsyncToken { Method = RedmineMethod.CreateWiki, Parameter = projectId, ResponseType = typeof(WikiPage), TokenId = id }); - } - return id; - } - - public Guid DeleteWikiPageAsync(string projectId, string pageName) - { - using (var wc = CreateWebClient(null)) - { - var id = Guid.NewGuid(); - wc.DownloadStringCompleted += WcDownloadStringCompleted; - wc.UploadStringAsync(new Uri(string.Format(WIKI_PAGE_FORMAT, host, projectId, pageName, mimeFormat)), DELETE, string.Empty, new AsyncToken { Method = RedmineMethod.DeleteObject, ResponseType = typeof(WikiPage), Parameter = id, TokenId = id }); - return id; - } - } - - /// - /// 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. - /// Returns the Guid associated with the async request. - public Guid UploadDataAsync(byte[] data) - { - using (var wc = CreateUploadWebClient(null)) - { - var id = Guid.NewGuid(); - wc.UploadDataCompleted += WcUploadDataCompleted; - wc.UploadDataAsync(new Uri(string.Format(FORMAT, host, "uploads", mimeFormat)), POST, data, new AsyncToken { Method = RedmineMethod.UploadData, ResponseType = typeof(Upload), TokenId = id }); - return id; - } - } - - /// - /// Adds an existing user to a group. This method does not block the calling thread. - /// - /// The group id. - /// The user id. - /// Returns the Guid associated with the async request. - public Guid AddUserToGroupAsync(int groupId, int userId) - { - using (var wc = CreateWebClient(null)) - { - var id = Guid.NewGuid(); - var asyncToken = new AsyncToken{Method = RedmineMethod.AddUserToGroup, Parameter = userId, TokenId = id}; - wc.DownloadStringCompleted += WcDownloadStringCompleted; - wc.UploadStringAsync(new Uri(string.Format(REQUEST_FORMAT, host, urls[typeof(Group)], groupId + "/users", mimeFormat)), POST, mimeFormat == MimeFormat.xml ? "" + userId + "" : "user_id:" + userId, asyncToken); - return id; - } - } - - /// - /// Removes an user from a group. This method does not block the calling thread. - /// - /// The group id. - /// The user id. - /// Returns the Guid associated with the async request. - public Guid DeleteUserFromGroupAsync(int groupId, int userId) - { - using (var wc = CreateWebClient(null)) - { - var id = Guid.NewGuid(); - wc.DownloadStringCompleted += WcDownloadStringCompleted; - wc.UploadStringAsync(new Uri(string.Format(REQUEST_FORMAT, host, urls[typeof(Group)], groupId + "/users/" + userId, mimeFormat)), DELETE, string.Empty, new AsyncToken { Method = RedmineMethod.DeleteUserFromGroup, Parameter = userId, TokenId = id }); - return id; - } - } - - public Guid GetObjectListAsync(NameValueCollection parameters) - { - if (!urls.ContainsKey(typeof(T))) return Guid.Empty; - - using (var wc = CreateWebClient(parameters)) - { - var id = Guid.NewGuid(); - var type = typeof(T); - wc.DownloadStringCompleted += WcDownloadStringCompleted; - - var asyncToken = new AsyncToken { Method = RedmineMethod.GetObjectList, ResponseType = type, TokenId = id, JsonRoot = urls[type] }; - - if (type == typeof(Version) || type == typeof(IssueCategory) || type == typeof(ProjectMembership)) - { - string projectId = GetOwnerId(parameters, "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."); - - wc.DownloadStringAsync(new Uri(string.Format(ENTITY_WITH_PARENT_FORMAT, host, "projects", projectId, urls[type], mimeFormat)), asyncToken); - } - else - if (type == typeof(IssueRelation)) - { - string issueId = GetOwnerId(parameters, "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"); - wc.DownloadStringAsync(new Uri(string.Format(ENTITY_WITH_PARENT_FORMAT, host, "issues", issueId, urls[type], mimeFormat)), asyncToken); - } - else - { - wc.DownloadStringAsync(new Uri(string.Format(FORMAT, host, urls[type], mimeFormat)), asyncToken); - } - return id; - } - } - - /// - /// Gets a Redmine object. This method does not block the calling thread. - /// - /// The type of objects to retrieve. - /// The id of the object. - /// Optional filters and/or optional fetched data. - /// Returns the Guid associated with the async request. - public Guid GetObjectAsync(string id, NameValueCollection parameters) where T : class - { - var type = typeof(T); - - if (!urls.ContainsKey(type)) return Guid.Empty; - - using (var wc = CreateWebClient(parameters)) - { - var guid = Guid.NewGuid(); - wc.DownloadStringCompleted += WcDownloadStringCompleted; - wc.DownloadStringAsync(new Uri(string.Format(REQUEST_FORMAT, host, urls[type], id, mimeFormat)), new AsyncToken { Method = RedmineMethod.GetObject, ResponseType = type, Parameter = id, TokenId = guid }); - return guid; - } - } - - /// - /// Creates a new Redmine object. This method does not block the calling thread. - /// - /// The type of object to create. - /// The object to create. - /// Returns the Guid associated with the async request. - public Guid CreateObjectAsync(T obj) where T : class,new() - { - var type = typeof(T); - - if (!urls.ContainsKey(type)) return Guid.Empty; - - var result = Serialize(obj); - - if (string.IsNullOrEmpty(result)) return Guid.Empty; - - using (var wc = CreateWebClient(null)) - { - var id = Guid.NewGuid(); - wc.DownloadStringCompleted += WcDownloadStringCompleted; - wc.UploadStringAsync(new Uri(string.Format(FORMAT, host, urls[type], mimeFormat)), POST, result, new AsyncToken { Method = RedmineMethod.CreateObject, ResponseType = type, Parameter = obj, TokenId = id }); - return id; - } - } - - /// - /// Updates a Redmine object. This method does not block the calling thread. - /// - /// The type of object to be update. - /// The id of the object to be update. - /// The object to be update. - /// - /// Returns the Guid associated with the async request. - public Guid UpdateObjectAsync(string id, T obj, string projectId = null) where T : class, new() - { - var type = typeof(T); - - if (!urls.ContainsKey(type)) return Guid.Empty; - - var request = Serialize(obj); - - if (string.IsNullOrEmpty(request)) return Guid.Empty; - - using (var wc = CreateWebClient(null)) - { - var guid = Guid.NewGuid(); - var asyncToken = new AsyncToken{Method = RedmineMethod.UpdateObject, ResponseType = type, Parameter = obj, TokenId = guid}; - wc.DownloadStringCompleted += WcDownloadStringCompleted; - if (type == typeof(Version) || type == typeof(IssueCategory) || type == typeof(ProjectMembership)) - { - if (string.IsNullOrEmpty(projectId)) throw new RedmineException("The project owner id is mandatory!"); - wc.UploadStringAsync(new Uri(string.Format(ENTITY_WITH_PARENT_FORMAT, host, "projects", projectId, urls[type], mimeFormat)), PUT, request, asyncToken); - } - else - wc.UploadStringAsync(new Uri(string.Format(REQUEST_FORMAT, host, urls[type], id, mimeFormat)), PUT, request, asyncToken); - return guid; - } - } - - /// - /// Deletes the Redmine object. This method does not block the calling thread. - /// - /// The type of objects to delete. - /// The id of the object to delete - /// Optional filters and/or optional fetched data. - /// Returns the Guid associated with the async request. - public Guid DeleteObjectAsync(string id, NameValueCollection parameters) where T : class - { - var type = typeof(T); - - if (!urls.ContainsKey(typeof(T))) return Guid.Empty; - - using (var wc = CreateWebClient(parameters)) - { - var guid = Guid.NewGuid(); - wc.DownloadStringCompleted += WcDownloadStringCompleted; - wc.UploadStringAsync(new Uri(string.Format(REQUEST_FORMAT, host, urls[type], id, mimeFormat)), DELETE, string.Empty, new AsyncToken { Method = RedmineMethod.DeleteObject, ResponseType = type, Parameter = id, TokenId = guid }); - return guid; - } - } - - private void WcDownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e) - { - var ut = e.UserState as AsyncToken; - if (e.Error != null) - HandleWebException((WebException)e.Error, ut.Method.ToString()); - else - if (!e.Cancelled) - { - ShowAsyncResult(e.Result, ut.ResponseType, ut.Method, ut.JsonRoot); - } - } - - private void WcUploadDataCompleted(object sender, UploadDataCompletedEventArgs e) - { - var ut = e.UserState as AsyncToken; - if (e.Error != null) - HandleWebException((WebException)e.Error, ut.Method.ToString()); - else - if (!e.Cancelled) - { - var responseString = Encoding.ASCII.GetString(e.Result); - ShowAsyncResult(responseString, ut.ResponseType, ut.Method, ut.JsonRoot); - } - } - - private void ShowAsyncResult(string response, Type responseType, RedmineMethod method, string jsonRoot) - { - var aev = new AsyncEventArgs(); - try - { - - if (mimeFormat == MimeFormat.json) - if (method == RedmineMethod.GetObjectList) - { - int totalItems; - aev.Result = RedmineSerialization.JsonDeserializeToList(response, jsonRoot, responseType, out totalItems); - aev.TotalItems = totalItems; - } - else - aev.Result = RedmineSerialization.JsonDeserialize(response, responseType, null); - else - if (method == RedmineMethod.GetObjectList) - { - using (var text = new StringReader(response)) - { - using (var xmlReader = new XmlTextReader(text)) - { - xmlReader.WhitespaceHandling = WhitespaceHandling.None; - xmlReader.Read(); - xmlReader.Read(); - - aev.TotalItems = xmlReader.ReadAttributeAsInt("total_count"); - - aev.Result = xmlReader.ReadElementContentAsCollection(responseType); - } - } - } - else - aev.Result = RedmineSerialization.FromXML(response, responseType); - - } - catch (ThreadAbortException ex) - { - aev.Error = ex.Message; - } - catch (Exception ex) - { - aev.Error = ex.Message; - } - if (DownloadCompleted != null) DownloadCompleted(this, aev); - } - } - - internal class AsyncToken - { - public Guid TokenId { get; set; } - public RedmineMethod Method { get; set; } - public Type ResponseType { get; set; } - public object Parameter { get; set; } - public string JsonRoot { get; set; } - } - - internal enum RedmineMethod - { - DeleteObject, - UpdateObject, - CreateObject, - GetObject, - GetObjectList, - DeleteUserFromGroup, - AddUserToGroup, - UploadData, - GetCurrentUser, - GetWikiPage, - GetAllWikis, - CreateWiki, - UpdateWiki, - DeleteWiki - } - - public class AsyncEventArgs : EventArgs - { - public string Error { get; set; } - public object Result { get; set; } - public int TotalItems { get; set; } - } -} \ No newline at end of file diff --git a/redmine-net40-api/RedmineSerialization.cs b/redmine-net40-api/RedmineSerialization.cs deleted file mode 100644 index cfadc34c..00000000 --- a/redmine-net40-api/RedmineSerialization.cs +++ /dev/null @@ -1,137 +0,0 @@ -/* - Copyright 2011 - 2015 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.IO; -using System.Xml; -using System.Xml.Serialization; - -namespace Redmine.Net.Api -{ - public static partial class RedmineSerialization - { - /// - /// 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. - /// - public 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. - public 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. - public static object FromXML(string xml, Type type) - { - using (var text = new StringReader(xml)) - { - var sr = new XmlSerializer(type); - return sr.Deserialize(text); - } - } - - /// - /// - /// - /// - /// 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-net40-api/RedmineSerializationJSON.cs b/redmine-net40-api/RedmineSerializationJSON.cs deleted file mode 100644 index 231c6976..00000000 --- a/redmine-net40-api/RedmineSerializationJSON.cs +++ /dev/null @@ -1,196 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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; -using System.Collections.Generic; -using System.Linq; -using System.Web.Script.Serialization; -using Redmine.Net.Api.JSonConverters; -using Redmine.Net.Api.Types; -using Version = Redmine.Net.Api.Types.Version; - -namespace Redmine.Net.Api -{ - public static partial class RedmineSerialization - { - private static readonly Dictionary converters = 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 (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 Converters { get { return converters; } } - - public static string JsonSerializer(T type) where T : new() - { - try - { - var ser = new JavaScriptSerializer(); - ser.RegisterConverters(new[] { converters[typeof(T)] }); - var jsonString = ser.Serialize(type); - return jsonString; - } - catch (Exception) - { - return null; - } - } - - /// - /// JSON Deserialization - /// - public static List JsonDeserializeToList(string jsonString, string root) where T : class, new() - { - int totalCount; - return JsonDeserializeToList(jsonString, root, out totalCount); - } - - public static object JsonDeserializeToList(string jsonString, string root, Type type, out int totalCount) - { - totalCount = 0; - if (String.IsNullOrEmpty(jsonString)) return null; - - var ser = new JavaScriptSerializer(); - ser.RegisterConverters(new[] { converters[type] }); - var dic = ser.Deserialize>(jsonString); - if (dic == null) return null; - - object obj, tc; - - if (dic.TryGetValue("total_count", out tc)) totalCount = (int)tc; - - if (dic.TryGetValue(root.ToLower(), out obj)) - { - var list = new ArrayList(); - if (type == typeof(Error)) - { - string info = null; - foreach (var item in (ArrayList)obj) - { - var arrayList = item as ArrayList; - if (arrayList != null) - { - foreach (var item2 in arrayList) info += item2 as string + " "; - } - else - info += item as string + " "; - } - var err = new Error { Info = info }; - list.Add(err); - } - else - { - AddToList(ser, list, type, obj); - } - return list; - } - return null; - } - - /// - /// 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(); - } - - private static void AddToList(JavaScriptSerializer ser, IList list, Type type, object obj) - { - foreach (var item in (ArrayList)obj) - { - if (item is ArrayList) - { - AddToList(ser, list, type, item); - } - else - { - var o = ser.ConvertToType(item, type); - list.Add(o); - } - } - } - - 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 ser = new JavaScriptSerializer(); - ser.RegisterConverters(new[] { converters[type] }); - - var dic = ser.Deserialize>(jsonString); - if (dic == null) return null; - - object obj; - if (dic.TryGetValue(root ?? type.Name.ToLower(), out obj)) - { - var deserializedObject = ser.ConvertToType(obj, type); - - return deserializedObject; - } - return null; - } - } -} \ No newline at end of file diff --git a/redmine-net40-api/RedmineWebClient.cs b/redmine-net40-api/RedmineWebClient.cs deleted file mode 100644 index 6206b9c8..00000000 --- a/redmine-net40-api/RedmineWebClient.cs +++ /dev/null @@ -1,51 +0,0 @@ -/* - Copyright 2011 - 2015 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.Net; - -namespace Redmine.Net.Api -{ - /// - /// - /// - public class RedmineWebClient :WebClient - { - private readonly CookieContainer container = new CookieContainer(); - - protected override WebRequest GetWebRequest(Uri address) - { - - Headers.Add(HttpRequestHeader.Cookie, "redmineCookie"); - - var wr = base.GetWebRequest(address); - var httpWebRequest = wr as HttpWebRequest; - - if (httpWebRequest != null) - { - httpWebRequest.KeepAlive = true; - httpWebRequest.CookieContainer = container; - - httpWebRequest.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; - - return httpWebRequest; - } - - return wr; - } - } -} \ No newline at end of file diff --git a/redmine-net40-api/redmine-net40-api.csproj b/redmine-net40-api/redmine-net40-api.csproj deleted file mode 100644 index 2cf42c7d..00000000 --- a/redmine-net40-api/redmine-net40-api.csproj +++ /dev/null @@ -1,235 +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 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - Types\Attachment.cs - - - Types\ChangeSet.cs - - - Types\CustomField.cs - - - Types\CustomFieldPossibleValue.cs - - - Types\CustomFieldRole.cs - - - Types\CustomFieldValue.cs - - - Types\Detail.cs - - - Types\Error.cs - - - Types\Group.cs - - - Types\GroupUser.cs - - - Types\Identifiable.cs - - - Types\IdentifiableName.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\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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Component - - - - - - \ 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 100644 index 835823ab..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 - 2015")] -[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.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/redmine-net45-api-signed/redmine-net-api.snk b/redmine-net45-api-signed/redmine-net-api.snk deleted file mode 100644 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 1328609a..00000000 --- a/redmine-net45-api-signed/redmine-net45-api-signed.csproj +++ /dev/null @@ -1,336 +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 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - true - - - redmine-net-api.snk - - - - - - - - - - - - - - Types\Attachment.cs - - - Types\ChangeSet.cs - - - Types\CustomField.cs - - - Types\CustomFieldPossibleValue.cs - - - Types\CustomFieldRole.cs - - - Types\CustomFieldValue.cs - - - Types\Detail.cs - - - Types\Error.cs - - - Types\Group.cs - - - Types\GroupUser.cs - - - Types\Identifiable.cs - - - Types\IdentifiableName.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\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 - - - ExtensionMethods.cs - - - JsonConverters\AttachmentConverter.cs - - - JsonConverters\ChangeSetConverter.cs - - - JsonConverters\CustomFieldConverter.cs - - - JsonConverters\CustomFieldPossibleValueConverter.cs - - - JsonConverters\CustomFieldRoleConverter.cs - - - JsonConverters\DetailConverter.cs - - - JsonConverters\ErrorConverter.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 - - - RedmineException.cs - - - RedmineManager.cs - - - RedmineSerialization.cs - - - RedmineSerializationJSON.cs - - - RedmineWebClient.cs - Component - - - RedmineAsyncWebClient.cs - - - - - - - - - \ 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 100644 index a5feeffa..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 - 2015")] -[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.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/redmine-net45-api/RedmineAsyncWebClient.cs b/redmine-net45-api/RedmineAsyncWebClient.cs deleted file mode 100644 index f56921be..00000000 --- a/redmine-net45-api/RedmineAsyncWebClient.cs +++ /dev/null @@ -1,59 +0,0 @@ -ο»Ώusing System; -using System.Net; -using System.Net.Cache; -using System.Threading; -using System.Threading.Tasks; - -namespace Redmine.Net.Api -{ - public class RedmineAsyncWebClient - { - public string BaseAddress { get; set; } - public RequestCachePolicy CachePolicy { get; set; } - public bool UseDefaultCredentials { get; set; } - public ICredentials Credentials { get; set; } - public WebHeaderCollection Headers { get; set; } - public IWebProxy Proxy { get; set; } - - public RedmineAsyncWebClient() - { - //var defaultClient = new WebClient(); - //BaseAddress = defaultClient.BaseAddress; - //Headers = defaultClient.Headers; - //Proxy = defaultClient.Proxy; - } - - public async Task DownloadStringAsync(string uri, CancellationToken cancelToken = default (CancellationToken), IProgress progress = null) - { - return await Task.Run(() => GetWebClient(cancelToken, progress).DownloadStringTaskAsync(uri)).ConfigureAwait(false); - } - - public async Task DownloadDataAsync(string uri, CancellationToken cancelToken = default (CancellationToken), IProgress progress = null) - { - return await Task.Run(() => GetWebClient(cancelToken, progress).DownloadDataTaskAsync(uri)).ConfigureAwait(false); - } - - public async Task DownloadFileAsync(string uri, string fileName, CancellationToken cancelToken = default (CancellationToken), IProgress progress = null) - { - await Task.Run(() => GetWebClient(cancelToken, progress).DownloadFileTaskAsync(uri, fileName)).ConfigureAwait(false); - } - - private WebClient GetWebClient(CancellationToken cancelToken, IProgress progress) - { - var wc = new WebClient - { - BaseAddress = BaseAddress, - CachePolicy = CachePolicy, - UseDefaultCredentials = UseDefaultCredentials, - Credentials = Credentials, - Headers = Headers, - Proxy = Proxy - }; - - if (cancelToken != CancellationToken.None) cancelToken.Register(() => wc.CancelAsync()); - if (progress != null) wc.DownloadProgressChanged += (sender, args) => progress.Report(args); - - return wc; - } - } -} diff --git a/redmine-net45-api/RedmineManagerAsync.cs b/redmine-net45-api/RedmineManagerAsync.cs deleted file mode 100644 index 232e0187..00000000 --- a/redmine-net45-api/RedmineManagerAsync.cs +++ /dev/null @@ -1,424 +0,0 @@ -ο»Ώ/* - Copyright 2011 - 2015 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.Collections.Specialized; -using System.IO; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Xml; -using Redmine.Net.Api.Types; -using Version = Redmine.Net.Api.Types.Version; - -namespace Redmine.Net.Api -{ - public partial class RedmineManager - { - public event EventHandler DownloadCompleted; - - public async Task GetCurrentUserAsync(NameValueCollection parameters = null) - { - string uri = string.Format(REQUEST_FORMAT, host, urls[typeof(User)], CURRENT_USER_URI, mimeFormat); - var result = await new RedmineAsyncWebClient().DownloadStringAsync(uri); - - return DeserializeResult(result); - } - - public async Task GetWikiPageAsync(string projectId, NameValueCollection parameters, string pageName, uint version = 0) - { - string uri = version == 0 - ? string.Format(WIKI_PAGE_FORMAT, host, projectId, pageName, mimeFormat) - : string.Format(WIKI_VERSION_FORMAT, host, projectId, pageName, version, mimeFormat); - - var result = await new RedmineAsyncWebClient().DownloadStringAsync(uri); - - return DeserializeResult(result); - } - - public Guid CreateOrUpdateWikiPageAsync(string projectId, string pageName, WikiPage wikiPage) - { - var result = Serialize(wikiPage); - - if (string.IsNullOrEmpty(result)) return Guid.Empty; - - var id = Guid.NewGuid(); - using (var wc = CreateWebClient(null)) - { - wc.DownloadStringCompleted += WcDownloadStringCompleted; - wc.UploadStringAsync(new Uri(string.Format(WIKI_PAGE_FORMAT, host, projectId, pageName, mimeFormat)), PUT, result, new AsyncToken { Method = RedmineMethod.CreateWiki, Parameter = projectId, ResponseType = typeof(WikiPage), TokenId = id }); - } - return id; - } - - public Guid DeleteWikiPageAsync(string projectId, string pageName) - { - using (var wc = CreateWebClient(null)) - { - var id = Guid.NewGuid(); - wc.DownloadStringCompleted += WcDownloadStringCompleted; - wc.UploadStringAsync(new Uri(string.Format(WIKI_PAGE_FORMAT, host, projectId, pageName, mimeFormat)), DELETE, string.Empty, new AsyncToken { Method = RedmineMethod.DeleteObject, ResponseType = typeof(WikiPage), Parameter = id, TokenId = id }); - return id; - } - } - - /// - /// 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. - /// Returns the Guid associated with the async request. - public Guid UploadDataAsync(byte[] data) - { - using (var wc = CreateUploadWebClient(null)) - { - var id = Guid.NewGuid(); - wc.UploadDataCompleted += WcUploadDataCompleted; - wc.UploadDataAsync(new Uri(string.Format(FORMAT, host, "uploads", mimeFormat)), POST, data, new AsyncToken { Method = RedmineMethod.UploadData, ResponseType = typeof(Upload), TokenId = id }); - return id; - } - } - - /// - /// Adds an existing user to a group. This method does not block the calling thread. - /// - /// The group id. - /// The user id. - /// Returns the Guid associated with the async request. - public Guid AddUserToGroupAsync(int groupId, int userId) - { - using (var wc = CreateWebClient(null)) - { - var id = Guid.NewGuid(); - var asyncToken = new AsyncToken { Method = RedmineMethod.AddUserToGroup, Parameter = userId, TokenId = id }; - wc.DownloadStringCompleted += WcDownloadStringCompleted; - wc.UploadStringAsync(new Uri(string.Format(REQUEST_FORMAT, host, urls[typeof(Group)], groupId + "/users", mimeFormat)), POST, mimeFormat == MimeFormat.xml ? "" + userId + "" : "user_id:" + userId, asyncToken); - return id; - } - } - - /// - /// Removes an user from a group. This method does not block the calling thread. - /// - /// The group id. - /// The user id. - /// Returns the Guid associated with the async request. - public Guid DeleteUserFromGroupAsync(int groupId, int userId) - { - using (var wc = CreateWebClient(null)) - { - var id = Guid.NewGuid(); - wc.DownloadStringCompleted += WcDownloadStringCompleted; - wc.UploadStringAsync(new Uri(string.Format(REQUEST_FORMAT, host, urls[typeof(Group)], groupId + "/users/" + userId, mimeFormat)), DELETE, string.Empty, new AsyncToken { Method = RedmineMethod.DeleteUserFromGroup, Parameter = userId, TokenId = id }); - return id; - } - } - - public async Task> GetObjectListAsync(NameValueCollection parameters) - { - if (!urls.ContainsKey(typeof(T))) return null; - - //using (var wc = CreateWebClient(parameters)) - //{ - // var id = Guid.NewGuid(); - // var type = typeof(T); - // wc.DownloadStringCompleted += WcDownloadStringCompleted; - - // var asyncToken = new AsyncToken { Method = RedmineMethod.GetObjectList, ResponseType = type, TokenId = id, JsonRoot = urls[type] }; - - // if (type == typeof(Version) || type == typeof(IssueCategory) || type == typeof(ProjectMembership)) - // { - // string projectId = GetOwnerId(parameters, "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."); - - // wc.DownloadStringAsync(new Uri(string.Format(ENTITY_WITH_PARENT_FORMAT, host, "projects", projectId, urls[type], mimeFormat)), asyncToken); - // } - // else - // if (type == typeof(IssueRelation)) - // { - // string issueId = GetOwnerId(parameters, "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"); - // wc.DownloadStringAsync(new Uri(string.Format(ENTITY_WITH_PARENT_FORMAT, host, "issues", issueId, urls[type], mimeFormat)), asyncToken); - // } - // else - // { - // wc.DownloadStringAsync(new Uri(string.Format(FORMAT, host, urls[type], mimeFormat)), asyncToken); - // } - // return id; - //} - - return null; - } - - /// - /// Gets a Redmine object. This method does not block the calling thread. - /// - /// The type of objects to retrieve. - /// The id of the object. - /// Optional filters and/or optional fetched data. - /// Returns the Guid associated with the async request. - public async Task GetObjectAsync(string id, NameValueCollection parameters) where T : class - { - var type = typeof(T); - - if (!urls.ContainsKey(type)) return default(T); - - //using (var wc = CreateWebClient(parameters)) - //{ - // var guid = Guid.NewGuid(); - // wc.DownloadStringCompleted += WcDownloadStringCompleted; - // wc.DownloadStringAsync(new Uri(string.Format(REQUEST_FORMAT, host, urls[type], id, mimeFormat)), new AsyncToken { Method = RedmineMethod.GetObject, ResponseType = type, Parameter = id, TokenId = guid }); - // return guid; - //} - - return null; - } - - /// - /// Creates a new Redmine object. This method does not block the calling thread. - /// - /// The type of object to create. - /// The object to create. - /// Returns the Guid associated with the async request. - public Guid CreateObjectAsync(T obj) where T : class,new() - { - var type = typeof(T); - - if (!urls.ContainsKey(type)) return Guid.Empty; - - var result = Serialize(obj); - - if (string.IsNullOrEmpty(result)) return Guid.Empty; - - using (var wc = CreateWebClient(null)) - { - var id = Guid.NewGuid(); - wc.DownloadStringCompleted += WcDownloadStringCompleted; - wc.UploadStringAsync(new Uri(string.Format(FORMAT, host, urls[type], mimeFormat)), POST, result, new AsyncToken { Method = RedmineMethod.CreateObject, ResponseType = type, Parameter = obj, TokenId = id }); - return id; - } - } - - /// - /// Updates a Redmine object. This method does not block the calling thread. - /// - /// The type of object to be update. - /// The id of the object to be update. - /// The object to be update. - /// - /// Returns the Guid associated with the async request. - public Guid UpdateObjectAsync(string id, T obj, string projectId = null) where T : class, new() - { - var type = typeof(T); - - if (!urls.ContainsKey(type)) return Guid.Empty; - - var request = Serialize(obj); - - if (string.IsNullOrEmpty(request)) return Guid.Empty; - - using (var wc = CreateWebClient(null)) - { - var guid = Guid.NewGuid(); - var asyncToken = new AsyncToken { Method = RedmineMethod.UpdateObject, ResponseType = type, Parameter = obj, TokenId = guid }; - wc.DownloadStringCompleted += WcDownloadStringCompleted; - if (type == typeof(Version) || type == typeof(IssueCategory) || type == typeof(ProjectMembership)) - { - if (string.IsNullOrEmpty(projectId)) throw new RedmineException("The project owner id is mandatory!"); - wc.UploadStringAsync(new Uri(string.Format(ENTITY_WITH_PARENT_FORMAT, host, "projects", projectId, urls[type], mimeFormat)), PUT, request, asyncToken); - } - else - wc.UploadStringAsync(new Uri(string.Format(REQUEST_FORMAT, host, urls[type], id, mimeFormat)), PUT, request, asyncToken); - return guid; - } - } - - /// - /// Deletes the Redmine object. This method does not block the calling thread. - /// - /// The type of objects to delete. - /// The id of the object to delete - /// Optional filters and/or optional fetched data. - /// Returns the Guid associated with the async request. - public Guid DeleteObjectAsync(string id, NameValueCollection parameters) where T : class - { - var type = typeof(T); - - if (!urls.ContainsKey(typeof(T))) return Guid.Empty; - - using (var wc = CreateWebClient(parameters)) - { - var guid = Guid.NewGuid(); - wc.DownloadStringCompleted += WcDownloadStringCompleted; - wc.UploadStringAsync(new Uri(string.Format(REQUEST_FORMAT, host, urls[type], id, mimeFormat)), DELETE, string.Empty, new AsyncToken { Method = RedmineMethod.DeleteObject, ResponseType = type, Parameter = id, TokenId = guid }); - return guid; - } - } - - private void WcDownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e) - { - var ut = e.UserState as AsyncToken; - if (e.Error != null) - HandleWebException((WebException)e.Error, ut.Method.ToString()); - else - if (!e.Cancelled) - { - ShowAsyncResult(e.Result, ut.ResponseType, ut.Method, ut.JsonRoot); - } - } - - private void WcUploadDataCompleted(object sender, UploadDataCompletedEventArgs e) - { - var ut = e.UserState as AsyncToken; - if (e.Error != null) - HandleWebException((WebException)e.Error, ut.Method.ToString()); - else - if (!e.Cancelled) - { - var responseString = Encoding.ASCII.GetString(e.Result); - ShowAsyncResult(responseString, ut.ResponseType, ut.Method, ut.JsonRoot); - } - } - - public T DeserializeResult(string result) - { - int totalItems = 0; - return DeserializeResult(result, out totalItems); - } - - public T DeserializeResult(string result, out int totalItems, string jsonRoot = null) - { - totalItems = 0; - if (string.IsNullOrWhiteSpace(result)) return default(T); - - var type = typeof(T); - if (type == typeof(List)) - { - if (mimeFormat == MimeFormat.json) - { - return (T)RedmineSerialization.JsonDeserializeToList(result, jsonRoot, type, out totalItems); - } - - using (var text = new StringReader(result)) - { - using (var xmlReader = new XmlTextReader(text)) - { - xmlReader.WhitespaceHandling = WhitespaceHandling.None; - xmlReader.Read(); - xmlReader.Read(); - - totalItems = xmlReader.ReadAttributeAsInt("total_count"); - - return (T)(object)xmlReader.ReadElementContentAsCollection(type); - } - } - } - else - { - if (mimeFormat == MimeFormat.json) - { - return (T)RedmineSerialization.JsonDeserialize(result, type, jsonRoot); - } - - return (T)RedmineSerialization.FromXML(result, type); - } - } - - private void ShowAsyncResult(string response, Type responseType, RedmineMethod method, string jsonRoot) - { - var aev = new AsyncEventArgs(); - try - { - - if (mimeFormat == MimeFormat.json) - if (method == RedmineMethod.GetObjectList) - { - int totalItems; - aev.Result = RedmineSerialization.JsonDeserializeToList(response, jsonRoot, responseType, out totalItems); - aev.TotalItems = totalItems; - } - else - aev.Result = RedmineSerialization.JsonDeserialize(response, responseType, null); - else - if (method == RedmineMethod.GetObjectList) - { - using (var text = new StringReader(response)) - { - using (var xmlReader = new XmlTextReader(text)) - { - xmlReader.WhitespaceHandling = WhitespaceHandling.None; - xmlReader.Read(); - xmlReader.Read(); - - aev.TotalItems = xmlReader.ReadAttributeAsInt("total_count"); - - aev.Result = xmlReader.ReadElementContentAsCollection(responseType); - } - } - } - else - aev.Result = RedmineSerialization.FromXML(response, responseType); - - } - catch (ThreadAbortException ex) - { - aev.Error = ex.Message; - } - catch (Exception ex) - { - aev.Error = ex.Message; - } - if (DownloadCompleted != null) DownloadCompleted(this, aev); - } - } - - internal class AsyncToken - { - public Guid TokenId { get; set; } - public RedmineMethod Method { get; set; } - public Type ResponseType { get; set; } - public object Parameter { get; set; } - public string JsonRoot { get; set; } - } - - internal enum RedmineMethod - { - DeleteObject, - UpdateObject, - CreateObject, - GetObject, - GetObjectList, - DeleteUserFromGroup, - AddUserToGroup, - UploadData, - GetCurrentUser, - GetWikiPage, - GetAllWikis, - CreateWiki, - UpdateWiki, - DeleteWiki - } - - public class AsyncEventArgs : EventArgs - { - public string Error { get; set; } - public object Result { get; set; } - public int TotalItems { get; set; } - } -} \ No newline at end of file diff --git a/redmine-net45-api/redmine-net45-api.csproj b/redmine-net45-api/redmine-net45-api.csproj deleted file mode 100644 index 485e9786..00000000 --- a/redmine-net45-api/redmine-net45-api.csproj +++ /dev/null @@ -1,327 +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 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - Types\Attachment.cs - - - Types\ChangeSet.cs - - - Types\CustomField.cs - - - Types\CustomFieldPossibleValue.cs - - - Types\CustomFieldRole.cs - - - Types\CustomFieldValue.cs - - - Types\Detail.cs - - - Types\Error.cs - - - Types\Group.cs - - - Types\GroupUser.cs - - - Types\Identifiable.cs - - - Types\IdentifiableName.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\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 - - - ExtensionMethods.cs - - - JSonConverters\AttachmentConverter.cs - - - JSonConverters\ChangeSetConverter.cs - - - JSonConverters\CustomFieldConverter.cs - - - JSonConverters\CustomFieldPossibleValueConverter.cs - - - JSonConverters\CustomFieldRoleConverter.cs - - - JSonConverters\DetailConverter.cs - - - JSonConverters\ErrorConverter.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 - - - RedmineException.cs - - - RedmineManager.cs - - - RedmineSerialization.cs - - - RedmineSerializationJSON.cs - - - RedmineWebClient.cs - Component - - - - - - - - - \ 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/TrackerCustomField.cs b/src/redmine-net-api/Authentication/IRedmineAuthentication.cs similarity index 57% rename from redmine-net20-api/Types/TrackerCustomField.cs rename to src/redmine-net-api/Authentication/IRedmineAuthentication.cs index 8df2ea27..d14d6fcf 100644 --- a/redmine-net20-api/Types/TrackerCustomField.cs +++ b/src/redmine-net-api/Authentication/IRedmineAuthentication.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2015 Adrian Popescu, Dorin Huzum. + 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,19 +14,27 @@ You may obtain a copy of the License at limitations under the License. */ -using System.Xml; -using System.Xml.Serialization; +using System.Net; -namespace Redmine.Net.Api.Types +namespace Redmine.Net.Api.Authentication; + +/// +/// +/// +public interface IRedmineAuthentication { - [XmlRoot("tracker")] - public class TrackerCustomField : Tracker - { - public override void ReadXml(XmlReader reader) - { - Id = reader.ReadAttributeAsInt("id"); - Name = reader.GetAttribute("name"); - reader.Read(); - } - } + /// + /// + /// + 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/redmine-net20-api/Types/CustomFieldPossibleValue.cs b/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs similarity index 53% rename from redmine-net20-api/Types/CustomFieldPossibleValue.cs rename to src/redmine-net-api/Authentication/RedmineNoAuthentication.cs index 913d1862..d8518828 100644 --- a/redmine-net20-api/Types/CustomFieldPossibleValue.cs +++ b/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2015 Adrian Popescu, Dorin Huzum. + 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,21 +14,22 @@ You may obtain a copy of the License at limitations under the License. */ -using System; -using System.Xml.Serialization; +using System.Net; +using Redmine.Net.Api.Extensions; -namespace Redmine.Net.Api.Types +namespace Redmine.Net.Api.Authentication; + +/// +/// +/// +public sealed class RedmineNoAuthentication: IRedmineAuthentication { - [XmlRoot("possible_value")] - public class CustomFieldPossibleValue : IEquatable - { - [XmlElement("value")] - public string Value { get; set; } - - public bool Equals(CustomFieldPossibleValue other) - { - if (other == null) return false; - return (Value == other.Value); - } - } + /// + 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/ProjectStatus.cs b/src/redmine-net-api/Common/IValue.cs old mode 100644 new mode 100755 similarity index 71% rename from redmine-net20-api/Types/ProjectStatus.cs rename to src/redmine-net-api/Common/IValue.cs index ad38d6b8..d95d24eb --- a/redmine-net20-api/Types/ProjectStatus.cs +++ b/src/redmine-net-api/Common/IValue.cs @@ -1,5 +1,5 @@ -ο»Ώ/* - Copyright 2011 - 2015 Adrian Popescu, Dorin Huzum. +/* + 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,12 +14,16 @@ You may obtain a copy of the License at limitations under the License. */ -namespace Redmine.Net.Api.Types +namespace Redmine.Net.Api.Common { - public enum ProjectStatus + /// + /// + /// + public interface IValue { - Active = 1, - Closed = 5, - Archived = 9 - } + /// + /// + /// + string Value{ get;} + } } \ No newline at end of file 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-net20-api/Types/Watcher.cs b/src/redmine-net-api/Http/Messages/RedmineApiResponse.cs similarity index 62% rename from redmine-net20-api/Types/Watcher.cs rename to src/redmine-net-api/Http/Messages/RedmineApiResponse.cs index e5e49170..71fb1948 100644 --- a/redmine-net20-api/Types/Watcher.cs +++ b/src/redmine-net-api/Http/Messages/RedmineApiResponse.cs @@ -1,5 +1,5 @@ -ο»Ώ/* - Copyright 2011 - 2015 Adrian Popescu, Dorin Huzum. +/* + 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.Xml.Serialization; +using System.Collections.Specialized; +using System.Net; -namespace Redmine.Net.Api.Types +namespace Redmine.Net.Api.Http.Messages; + +internal sealed class RedmineApiResponse { - [XmlRoot("user")] - public class Watcher : IdentifiableName - { - - } + 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/Types/Permission.cs b/src/redmine-net-api/Http/RedirectType.cs similarity index 60% rename from redmine-net20-api/Types/Permission.cs rename to src/redmine-net-api/Http/RedirectType.cs index ac7d9236..5bb7acf7 100644 --- a/redmine-net20-api/Types/Permission.cs +++ b/src/redmine-net-api/Http/RedirectType.cs @@ -1,5 +1,5 @@ -ο»Ώ/* - Copyright 2011 - 2015 Adrian Popescu, Dorin Huzum. +/* + 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,19 +14,24 @@ 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.Http { - [XmlRoot("permission")] - public class Permission + /// + /// + /// + internal enum RedirectType { - [XmlText] - public string Info { get; set; } - - public override string ToString() - { - return Info; - } + /// + /// + /// + 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/src/redmine-net-api/Internals/HashCodeHelper.cs b/src/redmine-net-api/Internals/HashCodeHelper.cs new file mode 100755 index 00000000..19f92d6a --- /dev/null +++ b/src/redmine-net-api/Internals/HashCodeHelper.cs @@ -0,0 +1,122 @@ +ο»Ώ/* + 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; + +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) 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 += 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 += 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; + + 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; + } + } + } +} \ No newline at end of file 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 91% rename from redmine-net20-api/Properties/AssemblyInfo.cs rename to src/redmine-net-api/Properties/AssemblyInfo.cs index acefe14b..f59d0821 100644 --- a/redmine-net20-api/Properties/AssemblyInfo.cs +++ b/src/redmine-net-api/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ ο»Ώusing System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -10,7 +9,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("redmine-net20-api")] -[assembly: AssemblyCopyright("Copyright Β© Adrian Popescu 2011 - 2015")] +[assembly: AssemblyCopyright("Copyright Β© Adrian Popescu 2011 - 2019")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -32,5 +31,5 @@ // 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")] +[assembly: AssemblyVersion("1.0.4")] +[assembly: AssemblyFileVersion("1.0.4")] 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/src/redmine-net-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs new file mode 100644 index 00000000..b5072aa6 --- /dev/null +++ b/src/redmine-net-api/RedmineKeys.cs @@ -0,0 +1,918 @@ +/* + 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 RedmineKeys + { + /// + /// + /// + public const string ACCOUNT = "account"; + /// + /// + /// + public const string ACTIVE = "active"; + /// + /// The activity + /// + public const string ACTIVITY = "activity"; + /// + /// + /// + public const string ACTIVITY_ID = "activity_id"; + /// + /// + /// + 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"; + /// + /// + /// + public const string ASSIGNED_TO_ID = "assigned_to_id"; + /// + /// + /// + public const string ASSIGNABLE = "assignable"; + /// + /// + /// + public const string ATTACHMENT = "attachment"; + /// + /// + /// + public const string ATTACHMENTS = "attachments"; + /// + /// + /// + public const string AUTHOR = "author"; + /// + /// + /// + public const string AUTH_SOURCE_ID = "auth_source_id"; + + /// + /// + /// + public const string AVATAR_URL = "avatar_url"; + /// + /// + /// + public const string CATEGORY = "category"; + /// + /// + /// + public const string CATEGORY_ID = "category_id"; + /// + /// + /// + public const string CHANGE_SET = "changeset"; + /// + /// + /// + public const string CHANGE_SETS = "changesets"; + /// + /// + /// + public const string CHILDREN = "children"; + /// + /// + /// + public const string CLOSE = "close"; + /// + /// + /// + public const string CLOSED_ON = "closed_on"; + /// + /// + /// + public const string COMMENT = "comment"; + /// + /// + /// + public const string COMMENTS = "comments"; + /// + /// + /// + public const string COMMITTED_ON = "committed_on"; + /// + /// + /// + public const string CONTENT = "content"; + /// + /// + /// + public const string CONTENT_TYPE = "content_type"; + /// + /// + /// + public const string CONTENT_URL = "content_url"; + /// + /// + /// + 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"; + + /// + /// + /// + public const string CUSTOMIZED_TYPE = "customized_type"; + /// + /// + /// + 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"; + /// + /// + /// + public const string DEFAULT_VALUE = "default_value"; + /// + /// + /// + public const string DEFAULT_VERSION = "default_version"; + /// + /// + /// + public const string DEFAULT_VERSION_ID = "default_version_id"; + /// + /// + /// + public const string DELAY = "delay"; + /// + /// + /// + public const string DESCRIPTION = "description"; + /// + /// + /// + public const string DETAIL = "detail"; + /// + /// + /// + public const string DETAILS = "details"; + /// + /// + /// + public const string DIGEST = "digest"; + /// + /// + /// + public const string DONE_RATIO = "done_ratio"; + /// + /// + /// + public const string DOCUMENT_CATEGORY = "document_category"; + /// + /// + /// + public const string DOCUMENTS = "documents"; + /// + /// + /// + public const string DOWNLOADS = "downloads"; + /// + /// + /// + public const string DUE_DATE = "due_date"; + /// + /// + /// + public const string ENABLED_MODULE = "enabled_module"; + /// + /// + /// + public const string ENABLED_MODULES = "enabled_modules"; + /// + /// + /// + public const string ENABLED_MODULE_NAMES = "enabled_module_names"; + /// + /// + /// + 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"; + /// + /// + /// + public const string ERRORS = "errors"; + /// + /// + /// + public const string ESTIMATED_HOURS = "estimated_hours"; + /// + /// + /// + public const string FIELD = "field"; + /// + /// + /// + public const string FIELD_FORMAT = "field_format"; + /// + /// + /// + public const string FILE = "file"; + /// + /// + /// + public const string FILE_NAME = "filename"; + /// + /// + /// + public const string FILE_SIZE = "filesize"; + + /// + /// + /// + public const string FILES = "files"; + /// + /// + /// + public const string FIRST_NAME = "firstname"; + /// + /// + /// + public const string FIXED_VERSION = "fixed_version"; + /// + /// + /// + public const string FIXED_VERSION_ID = "fixed_version_id"; + /// + /// + /// + public const string GENERATE_PASSWORD = "generate_password"; + /// + /// + /// + public const string GROUP = "group"; + /// + /// + /// + public const string GROUPS = "groups"; + /// + /// + /// + public const string GROUP_ID = "group_id"; + /// + /// + /// + public const string HOMEPAGE = "homepage"; + /// + /// + /// + public const string HOURS = "hours"; + /// + /// + /// + public const string ID = "id"; + /// + /// + /// + public const string IDENTIFIER = "identifier"; + /// + /// + /// + public const string INCLUDE = "include"; + /// + /// + /// + public const string INDEX = "index"; + /// + /// + /// + public const string INHERITED = "inherited"; + /// + /// + /// + public const string INHERIT_MEMBERS = "inherit_members"; + /// + /// + /// + public const string ISSUE = "issue"; + /// + /// + /// + public const string ISSUES = "issues"; + /// + /// + /// + public const string ISSUE_CATEGORIES = "issue_categories"; + /// + /// + /// + public const string ISSUE_CATEGORY = "issue_category"; + /// + /// + /// + 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"; + /// + /// + /// + public const string ISSUE_PRIORITIES = "issue_priorities"; + /// + /// + /// + public const string ISSUE_PRIORITY = "issue_priority"; + /// + /// + /// + public const string ISSUE_STATUS = "issue_status"; + /// + /// + /// + public const string ISSUE_STATUSES = "issue_statuses"; + /// + /// + /// + public const string ISSUE_TO_ID = "issue_to_id"; + /// + /// + /// + public const string ISSUES_VISIBILITY = "issues_visibility"; + + /// + /// + /// + public const string IS_CLOSED = "is_closed"; + /// + /// + /// + public const string IS_DEFAULT = "is_default"; + /// + /// + /// + public const string IS_FILTER = "is_filter"; + /// + /// + /// + public const string IS_PRIVATE = "is_private"; + /// + /// + /// + public const string IS_PUBLIC = "is_public"; + /// + /// + /// + public const string IS_REQUIRED = "is_required"; + /// + /// + /// + public const string JOURNAL = "journal"; + /// + /// + /// + public const string JOURNALS = "journals"; + /// + /// + /// + public const string KEY = "key"; + /// + /// + /// + public const string LABEL = "label"; + /// + /// + /// + public const string LAST_NAME = "lastname"; + /// + /// + /// + public const string LAST_LOGIN_ON = "last_login_on"; + /// + /// + /// + public const string LIMIT = "limit"; + /// + /// + /// + public const string LOGIN = "login"; + /// + /// + /// + public const string MAIL = "mail"; + /// + /// + /// + public const string MAIL_NOTIFICATION = "mail_notification"; + /// + /// + /// + public const string MAX_LENGTH = "max_length"; + /// + /// + /// + public const string MEMBERSHIP = "membership"; + /// + /// + /// + public const string MEMBERSHIPS = "memberships"; + /// + /// + /// + public const string MESSAGES = "messages"; + /// + /// + /// + public const string MIN_LENGTH = "min_length"; + /// + /// + /// + public const string MULTIPLE = "multiple"; + /// + /// + /// + public const string MUST_CHANGE_PASSWORD = "must_change_passwd"; + /// + /// + /// + public const string MY = "my"; + /// + /// + /// + public const string NAME = "name"; + /// + /// + /// + public const string NEWS = "news"; + /// + /// + /// + public const string NEW_VALUE = "new_value"; + /// + /// + /// + public const string NOTES = "notes"; + /// + /// + /// + public const string OFFSET = "offset"; + /// + /// + /// + public const string OLD_VALUE = "old_value"; + /// + /// + /// + public const string OPEN_ISSUES = "open_issues"; + /// + /// + /// + public const string PARENT = "parent"; + /// + /// + /// + public const string PARENT_ID = "parent_id"; + /// + /// + /// + public const string PARENT_ISSUE_ID = "parent_issue_id"; + /// + /// + /// + public const string PASSWORD = "password"; + /// + /// + /// + public const string PASSWORD_CHANGED_ON = "passwd_changed_on"; + /// + /// + /// + public const string PERMISSION = "permission"; + /// + /// + /// + public const string PERMISSIONS = "permissions"; + /// + /// + /// + public const string POSSIBLE_VALUE = "possible_value"; + /// + /// + /// + public const string POSSIBLE_VALUES = "possible_values"; + /// + /// + /// + public const string PRIORITY = "priority"; + /// + /// + /// + public const string PRIORITY_ID = "priority_id"; + /// + /// + /// + public const string PRIVATE_NOTES = "private_notes"; + /// + /// + /// + public const string PROJECT = "project"; + /// + /// + /// + public const string PROJECTS = "projects"; + /// + /// + /// + public const string PROJECT_ID = "project_id"; + /// + /// + /// + public const string PROPERTY = "property"; + /// + /// + /// + 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"; + /// + /// + /// + public const string RELATION = "relation"; + /// + /// + /// + public const string RELATIONS = "relations"; + /// + /// + /// + public const string RELATION_TYPE = "relation_type"; + /// + /// + /// + 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"; + /// + /// + /// + public const string ROLES = "roles"; + /// + /// + /// + public const string ROLE_ID = "role_id"; + /// + /// + /// + public const string ROLE_IDS = "role_ids"; + /// + /// + /// + 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"; + /// + /// + /// + public const string SORT = "sort"; + /// + /// + /// + public const string SPENT_HOURS = "spent_hours"; + /// + /// + /// + public const string SPENT_ON = "spent_on"; + /// + /// + /// + public const string START_DATE = "start_date"; + /// + /// + /// + public const string STATUS = "status"; + /// + /// + /// + public const string STATUS_ID = "status_id"; + /// + /// + /// + public const string SUBJECT = "subject"; + /// + /// + /// + public const string SUB_PROJECT_ID = "subproject_id"; + /// + /// + /// + public const string SUMMARY = "summary"; + /// + /// + /// + public const string TEXT = "text"; + /// + /// + /// + public const string TIME_ENTRY = "time_entry"; + /// + /// + /// + public const string TIME_ENTRIES = "time_entries"; + /// + /// + /// + public const string TIME_ENTRY_ACTIVITIES = "time_entry_activities"; + /// + /// + /// + public const string TIME_ENTRY_ACTIVITY = "time_entry_activity"; + /// + /// + /// + 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"; + /// + /// + /// + public const string TOTAL_COUNT = "total_count"; + /// + /// + /// + public const string TOTAL_ESTIMATED_HOURS = "total_estimated_hours"; + /// + /// + /// + public const string TOTAL_SPENT_HOURS = "total_spent_hours"; + /// + /// + /// + public const string TRACKER = "tracker"; + /// + /// + /// + public const string TRACKERS = "trackers"; + /// + /// + /// + public const string TRACKER_ID = "tracker_id"; + /// + /// + /// + 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"; + /// + /// + /// + public const string UPDATED_ON = "updated_on"; + /// + /// + /// + public const string UPDATED_BY = "updated_by"; + /// + /// + /// + public const string UPLOAD = "upload"; + /// + /// + /// + public const string UPLOADS = "uploads"; + /// + /// + /// + public const string URL = "url"; + /// + /// + /// + public const string USER = "user"; + /// + /// + /// + public const string USERS = "users"; + /// + /// + /// + public const string USER_ID = "user_id"; + /// + /// + /// + public const string USER_IDS = "user_ids"; + /// + /// + /// + public const string USERS_VISIBILITY = "users_visibility"; + /// + /// + /// + public const string VALUE = "value"; + /// + /// + /// + public const string VERSION = "version"; + /// + /// + /// + public const string VERSION_ID = "version_id"; + /// + /// + /// + public const string VERSIONS = "versions"; + /// + /// + /// + public const string VISIBLE = "visible"; + /// + /// + /// + public const string WATCHER = "watcher"; + /// + /// + /// + public const string WATCHERS = "watchers"; + /// + /// + /// + public const string WATCHER_USER_IDS = "watcher_user_ids"; + /// + /// + /// + 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"; + } +} \ 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/src/redmine-net-api/Serialization/Json/IJsonSerializable.cs b/src/redmine-net-api/Serialization/Json/IJsonSerializable.cs new file mode 100644 index 00000000..bf6b25b7 --- /dev/null +++ b/src/redmine-net-api/Serialization/Json/IJsonSerializable.cs @@ -0,0 +1,37 @@ +/* + 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 Newtonsoft.Json; + +namespace Redmine.Net.Api.Serialization.Json +{ + /// + /// + /// + public interface IJsonSerializable + { + /// + /// + /// + /// + void WriteJson(JsonWriter writer); + /// + /// + /// + /// + 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/CustomFieldValue.cs b/src/redmine-net-api/Serialization/SerializationType.cs similarity index 58% rename from redmine-net20-api/Types/CustomFieldValue.cs rename to src/redmine-net-api/Serialization/SerializationType.cs index 183e281d..3830d3fe 100644 --- a/redmine-net20-api/Types/CustomFieldValue.cs +++ b/src/redmine-net-api/Serialization/SerializationType.cs @@ -1,5 +1,5 @@ -ο»Ώ/* - Copyright 2011 - 2015 Adrian Popescu, Dorin Huzum. +/* + 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,19 +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 { - [XmlRoot("value")] - public class CustomFieldValue + /// + /// Specifies the serialization types supported by the Redmine API. + /// + public enum SerializationType { - [XmlText] - public string Info { get; set; } + /// + /// The XML format. + /// + Xml, - public override string ToString() - { - return Info; - } + /// + /// The JSON format. + /// + 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/redmine-net20-api/Types/GroupUser.cs b/src/redmine-net-api/Serialization/Xml/IXmlSerializerCache.cs similarity index 52% rename from redmine-net20-api/Types/GroupUser.cs rename to src/redmine-net-api/Serialization/Xml/IXmlSerializerCache.cs index 7134928d..a11e2d33 100644 --- a/redmine-net20-api/Types/GroupUser.cs +++ b/src/redmine-net-api/Serialization/Xml/IXmlSerializerCache.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2015 Adrian Popescu, Dorin Huzum. + 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. @@ -17,20 +17,18 @@ limitations under the License. using System; using System.Xml.Serialization; -namespace Redmine.Net.Api.Types +namespace Redmine.Net.Api.Serialization.Xml { - [XmlRoot("user")] - public class GroupUser : IdentifiableName, IEquatable + internal interface IXmlSerializerCache { - public bool Equals(GroupUser other) - { - if (other == null) return false; - return Id == other.Id && Name == other.Name; - } - - public override string ToString() - { - return Id + ", " + Name; - } + 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/src/redmine-net-api/Types/CustomFieldRole.cs b/src/redmine-net-api/Types/CustomFieldRole.cs new file mode 100644 index 00000000..7b1b20a9 --- /dev/null +++ b/src/redmine-net-api/Types/CustomFieldRole.cs @@ -0,0 +1,48 @@ +/* + 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.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.ROLE)] + public sealed class CustomFieldRole : IdentifiableName + { + /// + /// Initializes a new instance of the class. + /// + /// Serialization + public CustomFieldRole() { } + + internal CustomFieldRole(int id, string name) + : base(id, name) + { + } + + /// + /// + /// + /// + 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/src/redmine-net-api/Types/Error.cs b/src/redmine-net-api/Types/Error.cs new file mode 100644 index 00000000..2c6db1c8 --- /dev/null +++ b/src/redmine-net-api/Types/Error.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.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 sealed class Error : IXmlSerializable, IJsonSerializable, IEquatable + { + /// + /// + /// + public Error() { } + + /// + /// + /// + internal Error(string info) + { + Info = info; + } + + #region Properties + /// + /// + /// + public string Info { get; private set; } + #endregion + + #region Implementation of IXmlSerialization + /// + /// + /// + /// + public XmlSchema GetSchema() { return null; } + + /// + /// + /// + /// + public void ReadXml(XmlReader reader) + { + while (!reader.EOF) + { + switch (reader.Name) + { + case RedmineKeys.ERROR: 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) + { + 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); + } + + /// + /// + /// + /// + /// + 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 Error); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + 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/src/redmine-net-api/Types/GroupUser.cs b/src/redmine-net-api/Types/GroupUser.cs new file mode 100644 index 00000000..0151058b --- /dev/null +++ b/src/redmine-net-api/Types/GroupUser.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. +*/ + +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.USER)] + public sealed class GroupUser : IdentifiableName, IValue + { + #region Implementation of IValue + /// + /// + /// + public string Value => Id.ToInvariantString(); + #endregion + + /// + /// + /// + /// + 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/src/redmine-net-api/Types/IssuePriority.cs b/src/redmine-net-api/Types/IssuePriority.cs new file mode 100644 index 00000000..914ffacd --- /dev/null +++ b/src/redmine-net-api/Types/IssuePriority.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; + +namespace Redmine.Net.Api.Types +{ + /// + /// Availability 2.2 + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.ISSUE_PRIORITY)] + public sealed class IssuePriority : + IdentifiableName + ,IEquatable + { + #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.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 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.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 + /// + /// + /// + /// + /// + public bool Equals(IssuePriority 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 IssuePriority); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(IsDefault, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsActive, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(IssuePriority left, IssuePriority right) + { + 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/src/redmine-net-api/Types/IssueRelationType.cs b/src/redmine-net-api/Types/IssueRelationType.cs new file mode 100644 index 00000000..01d06292 --- /dev/null +++ b/src/redmine-net-api/Types/IssueRelationType.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.Xml.Serialization; + +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, + + /// + /// + /// + Duplicates, + + /// + /// + /// + Duplicated, + + /// + /// + /// + Blocks, + + /// + /// + /// + Blocked, + + /// + /// + /// + Precedes, + + /// + /// + /// + Follows, + + /// + /// + /// + + [XmlEnum(RedmineKeys.COPIED_TO)] + CopiedTo, + + /// + /// + /// + [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/src/redmine-net-api/Types/Permission.cs b/src/redmine-net-api/Types/Permission.cs new file mode 100644 index 00000000..bf411f5e --- /dev/null +++ b/src/redmine-net-api/Types/Permission.cs @@ -0,0 +1,158 @@ +/* + 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.PERMISSION)] + #pragma warning disable CA1711 + public sealed class Permission : IXmlSerializable, IJsonSerializable, IEquatable + #pragma warning restore CA1711 + { + #region Properties + /// + /// + /// + public string Info { get; internal set; } + #endregion + + #region Implementation of IXmlSerializable + + /// + /// + /// + /// + public XmlSchema GetSchema() { return null; } + + /// + /// + /// + /// + public void ReadXml(XmlReader reader) + { + reader.Read(); + if (reader.NodeType == XmlNodeType.Text) + { + Info = reader.Value; + } + } + + /// + /// + /// + /// + public void WriteXml(XmlWriter writer) { } + + #endregion + + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public void ReadJson(JsonReader reader) + { + if (reader.TokenType == JsonToken.String) + { + Info = reader.Value as string; + } + } + + /// + /// + /// + /// + public void WriteJson(JsonWriter writer) { } + #endregion + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public bool Equals(Permission other) + { + return other != null && 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 Permission); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Info, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Permission left, Permission right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Permission left, Permission right) + { + 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/ProjectEnabledModule.cs b/src/redmine-net-api/Types/ProjectIssueCategory.cs similarity index 51% rename from redmine-net20-api/Types/ProjectEnabledModule.cs rename to src/redmine-net-api/Types/ProjectIssueCategory.cs index 3e2f01be..6f214692 100644 --- a/redmine-net20-api/Types/ProjectEnabledModule.cs +++ b/src/redmine-net-api/Types/ProjectIssueCategory.cs @@ -1,5 +1,5 @@ -ο»Ώ /* - Copyright 2011 - 2015 Adrian Popescu, Dorin Huzum. +ο»Ώ/* + 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,26 +14,34 @@ 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("enabled_module")] - public class ProjectEnabledModule : IdentifiableName, IEquatable + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.ISSUE_CATEGORY)] + public sealed class ProjectIssueCategory : IdentifiableName { - public bool Equals(ProjectEnabledModule other) - { - if (other == null) return false; - return Id == other.Id && Name == other.Name; - } + /// + /// + /// + public ProjectIssueCategory() { } - public override string ToString() + internal ProjectIssueCategory(int id, string name) + : base(id, name) { - return Id + ", " + Name; } + + /// + /// + /// + /// + 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/src/redmine-net-api/Types/ProjectStatus.cs b/src/redmine-net-api/Types/ProjectStatus.cs new file mode 100755 index 00000000..1e98e1bb --- /dev/null +++ b/src/redmine-net-api/Types/ProjectStatus.cs @@ -0,0 +1,41 @@ +ο»Ώ/* + 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.Types +{ + /// + /// + /// + public enum ProjectStatus + { + /// + /// value of zero - Not set/unknown + /// + None, + /// + /// + /// + Active = 1, + /// + /// + /// + Closed = 5, + /// + /// + /// + Archived = 9 + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs b/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs new file mode 100644 index 00000000..823de240 --- /dev/null +++ b/src/redmine-net-api/Types/ProjectTimeEntryActivity.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.Diagnostics; +using System.Xml.Serialization; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [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) + { + } + + /// + /// + /// + /// + 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/ProjectTracker.cs b/src/redmine-net-api/Types/UserGroup.cs similarity index 60% rename from redmine-net20-api/Types/ProjectTracker.cs rename to src/redmine-net-api/Types/UserGroup.cs index b122e72d..55697d26 100644 --- a/redmine-net20-api/Types/ProjectTracker.cs +++ b/src/redmine-net-api/Types/UserGroup.cs @@ -1,5 +1,5 @@ -ο»Ώ/* - Copyright 2011 - 2015 Adrian Popescu, Dorin Huzum. +/* + 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,26 +14,24 @@ 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("tracker")] - public class ProjectTracker : IdentifiableName, IEquatable + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + [XmlRoot(RedmineKeys.GROUP)] + public sealed class UserGroup : IdentifiableName { - public bool Equals(ProjectTracker other) - { - if (other == null) return false; - return Id == other.Id && Name == other.Name; - } + /// + /// + /// + /// + private string DebuggerDisplay => $"[UserGroup: Id={Id.ToInvariantString()}, Name={Name}]"; - public override string ToString() - { - return Id + ", " + Name; - } } } \ No newline at end of file diff --git a/redmine-net20-api/Types/UserStatus.cs b/src/redmine-net-api/Types/UserStatus.cs similarity index 65% rename from redmine-net20-api/Types/UserStatus.cs rename to src/redmine-net-api/Types/UserStatus.cs index 617c2c33..c14b6a38 100644 --- a/redmine-net20-api/Types/UserStatus.cs +++ b/src/redmine-net-api/Types/UserStatus.cs @@ -1,5 +1,5 @@ ο»Ώ/* - Copyright 2011 - 2015 Adrian Popescu, Dorin Huzum. + 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. @@ -16,11 +16,22 @@ limitations under the License. namespace Redmine.Net.Api.Types { + /// + /// + /// public enum UserStatus { - STATUS_ANONYMOUS = 0, - STATUS_ACTIVE = 1, - STATUS_REGISTERED = 2, - STATUS_LOCKED = 3 + /// + /// + /// + StatusActive = 1, + /// + /// + /// + StatusRegistered = 2, + /// + /// + /// + 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/Types/ProjectIssueCategory.cs b/src/redmine-net-api/Types/VersionSharing.cs similarity index 55% rename from redmine-net20-api/Types/ProjectIssueCategory.cs rename to src/redmine-net-api/Types/VersionSharing.cs index d74a1706..d68e8236 100644 --- a/redmine-net20-api/Types/ProjectIssueCategory.cs +++ b/src/redmine-net-api/Types/VersionSharing.cs @@ -1,5 +1,5 @@ ο»Ώ/* - Copyright 2011 - 2015 Adrian Popescu, Dorin Huzum. + 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,26 +14,36 @@ You may obtain a copy of the License at limitations under the License. */ -using System; -using System.Xml.Serialization; - namespace Redmine.Net.Api.Types { /// /// /// - [XmlRoot("issue_category")] - public class ProjectIssueCategory : IdentifiableName, IEquatable + public enum VersionSharing { - public bool Equals(ProjectTracker other) - { - if (other == null) return false; - return Id == other.Id && Name == other.Name; - } - - public override string ToString() - { - return Id + ", " + Name; - } + /// + /// + /// + Unknown = 0, + /// + /// + /// + None = 1, + /// + /// + /// + Descendants, + /// + /// + /// + Hierarchy, + /// + /// + /// + Tree, + /// + /// + /// + System } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/VersionStatus.cs b/src/redmine-net-api/Types/VersionStatus.cs new file mode 100644 index 00000000..69c42ce9 --- /dev/null +++ b/src/redmine-net-api/Types/VersionStatus.cs @@ -0,0 +1,41 @@ +ο»Ώ/* + 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.Types +{ + /// + /// + /// + public enum VersionStatus + { + /// + /// value of zero - Not set/unknown + /// + None, + /// + /// + /// + Open = 1, + /// + /// + /// + Locked, + /// + /// + /// + Closed + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/Watcher.cs b/src/redmine-net-api/Types/Watcher.cs new file mode 100644 index 00000000..57607455 --- /dev/null +++ b/src/redmine-net-api/Types/Watcher.cs @@ -0,0 +1,132 @@ +/* + 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; +using Redmine.Net.Api.Internals; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [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 + /// + /// + /// + /// + 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(Watcher 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 Watcher); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + return hashCode; + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Watcher left, Watcher right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Watcher left, Watcher right) + { + return !Equals(left, right); + } + #endregion + + /// + /// + /// + /// + 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/ExtensionAttribute.cs b/src/redmine-net-api/_net20/ExtensionAttribute.cs old mode 100644 new mode 100755 similarity index 78% rename from redmine-net20-api/ExtensionAttribute.cs rename to src/redmine-net-api/_net20/ExtensionAttribute.cs index 1f9a1cd7..4c43dcfc --- a/redmine-net20-api/ExtensionAttribute.cs +++ b/src/redmine-net-api/_net20/ExtensionAttribute.cs @@ -1,5 +1,6 @@ -ο»Ώ/* - Copyright 2011 - 2015 Adrian Popescu, Dorin Huzum.. +ο»Ώ#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. @@ -16,8 +17,15 @@ limitations under the License. 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/src/redmine-net-api/_net20/Func.cs b/src/redmine-net-api/_net20/Func.cs new file mode 100644 index 00000000..39095ef6 --- /dev/null +++ b/src/redmine-net-api/_net20/Func.cs @@ -0,0 +1,72 @@ +ο»Ώ/* + 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 +// ReSharper disable once CheckNamespace +namespace System +{ + /// + /// + /// + /// The type of the result. + /// + public delegate TResult Func(); + /// + /// + /// + /// + /// The type of the result. + /// a. + /// + public delegate TResult Func(T a); + /// + /// + /// + /// The type of the 1. + /// The type of the 2. + /// The type of the result. + /// The arg1. + /// The arg2. + /// + public delegate TResult Func(T1 arg1, T2 arg2); + /// + /// + /// + /// The type of the 1. + /// The type of the 2. + /// The type of the 3. + /// The type of the result. + /// The arg1. + /// The arg2. + /// The arg3. + /// + public delegate TResult Func(T1 arg1, T2 arg2, T3 arg3); + /// + /// + /// + /// The type of the 1. + /// The type of the 2. + /// The type of the 3. + /// The type of the 4. + /// The type of the result. + /// The arg1. + /// The arg2. + /// The arg3. + /// The arg4. + /// + public delegate TResult Func(T1 arg1, T2 arg2, T3 arg3, T4 arg4); +} +#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/stylesheets/github-dark.css b/stylesheets/github-dark.css deleted file mode 100644 index 0c393bfa..00000000 --- a/stylesheets/github-dark.css +++ /dev/null @@ -1,116 +0,0 @@ -/* - Copyright 2014 GitHub Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -.pl-c /* comment */ { - color: #969896; -} - -.pl-c1 /* constant, markup.raw, meta.diff.header, meta.module-reference, meta.property-name, support, support.constant, support.variable, variable.other.constant */, -.pl-s .pl-v /* string variable */ { - color: #0099cd; -} - -.pl-e /* entity */, -.pl-en /* entity.name */ { - color: #9774cb; -} - -.pl-s .pl-s1 /* string source */, -.pl-smi /* storage.modifier.import, storage.modifier.package, storage.type.java, variable.other, variable.parameter.function */ { - color: #ddd; -} - -.pl-ent /* entity.name.tag */ { - color: #7bcc72; -} - -.pl-k /* keyword, storage, storage.type */ { - color: #cc2372; -} - -.pl-pds /* punctuation.definition.string, string.regexp.character-class */, -.pl-s /* string */, -.pl-s .pl-pse .pl-s1 /* string punctuation.section.embedded source */, -.pl-sr /* string.regexp */, -.pl-sr .pl-cce /* string.regexp constant.character.escape */, -.pl-sr .pl-sra /* string.regexp string.regexp.arbitrary-repitition */, -.pl-sr .pl-sre /* string.regexp source.ruby.embedded */ { - color: #3c66e2; -} - -.pl-v /* variable */ { - color: #fb8764; -} - -.pl-id /* invalid.deprecated */ { - color: #e63525; -} - -.pl-ii /* invalid.illegal */ { - background-color: #e63525; - color: #f8f8f8; -} - -.pl-sr .pl-cce /* string.regexp constant.character.escape */ { - color: #7bcc72; - font-weight: bold; -} - -.pl-ml /* markup.list */ { - color: #c26b2b; -} - -.pl-mh /* markup.heading */, -.pl-mh .pl-en /* markup.heading entity.name */, -.pl-ms /* meta.separator */ { - color: #264ec5; - font-weight: bold; -} - -.pl-mq /* markup.quote */ { - color: #00acac; -} - -.pl-mi /* markup.italic */ { - color: #ddd; - font-style: italic; -} - -.pl-mb /* markup.bold */ { - color: #ddd; - font-weight: bold; -} - -.pl-md /* markup.deleted, meta.diff.header.from-file */ { - background-color: #ffecec; - color: #bd2c00; -} - -.pl-mi1 /* markup.inserted, meta.diff.header.to-file */ { - background-color: #eaffea; - color: #55a532; -} - -.pl-mdr /* meta.diff.range */ { - color: #9774cb; - font-weight: bold; -} - -.pl-mo /* meta.output */ { - color: #264ec5; -} - diff --git a/stylesheets/ie.css b/stylesheets/ie.css deleted file mode 100644 index 43882f2e..00000000 --- a/stylesheets/ie.css +++ /dev/null @@ -1,3 +0,0 @@ -nav { - display: none; -} diff --git a/stylesheets/normalize.css b/stylesheets/normalize.css deleted file mode 100644 index 16a13512..00000000 --- a/stylesheets/normalize.css +++ /dev/null @@ -1,459 +0,0 @@ -/* normalize.css 2012-02-07T12:37 UTC - https://github.com/necolas/normalize.css */ -/* ============================================================================= - HTML5 display definitions - ========================================================================== */ -/* - * Corrects block display not defined in IE6/7/8/9 & FF3 - */ -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -nav, -section, -summary { - display: block; -} - -/* - * Corrects inline-block display not defined in IE6/7/8/9 & FF3 - */ -audio, -canvas, -video { - display: inline-block; - *display: inline; - *zoom: 1; -} - -/* - * Prevents modern browsers from displaying 'audio' without controls - */ -audio:not([controls]) { - display: none; -} - -/* - * Addresses styling for 'hidden' attribute not present in IE7/8/9, FF3, S4 - * Known issue: no IE6 support - */ -[hidden] { - display: none; -} - -/* ============================================================================= - Base - ========================================================================== */ -/* - * 1. Corrects text resizing oddly in IE6/7 when body font-size is set using em units - * http://clagnut.com/blog/348/#c790 - * 2. Prevents iOS text size adjust after orientation change, without disabling user zoom - * www.456bereastreet.com/archive/201012/controlling_text_size_in_safari_for_ios_without_disabling_user_zoom/ - */ -html { - font-size: 100%; - /* 1 */ - -webkit-text-size-adjust: 100%; - /* 2 */ - -ms-text-size-adjust: 100%; - /* 2 */ -} - -/* - * Addresses font-family inconsistency between 'textarea' and other form elements. - */ -html, -button, -input, -select, -textarea { - font-family: sans-serif; -} - -/* - * Addresses margins handled incorrectly in IE6/7 - */ -body { - margin: 0; -} - -/* ============================================================================= - Links - ========================================================================== */ -/* - * Addresses outline displayed oddly in Chrome - */ -a:focus { - outline: thin dotted; -} - -/* - * Improves readability when focused and also mouse hovered in all browsers - * people.opera.com/patrickl/experiments/keyboard/test - */ -a:hover, -a:active { - outline: 0; -} - -/* ============================================================================= - Typography - ========================================================================== */ -/* - * Addresses font sizes and margins set differently in IE6/7 - * Addresses font sizes within 'section' and 'article' in FF4+, Chrome, S5 - */ -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -h2 { - font-size: 1.5em; - margin: 0.83em 0; -} - -h3 { - font-size: 1.17em; - margin: 1em 0; -} - -h4 { - font-size: 1em; - margin: 1.33em 0; -} - -h5 { - font-size: 0.83em; - margin: 1.67em 0; -} - -h6 { - font-size: 0.75em; - margin: 2.33em 0; -} - -/* - * Addresses styling not present in IE7/8/9, S5, Chrome - */ -abbr[title] { - border-bottom: 1px dotted; -} - -/* - * Addresses style set to 'bolder' in FF3+, S4/5, Chrome -*/ -b, -strong { - font-weight: bold; -} - -blockquote { - margin: 1em 40px; -} - -/* - * Addresses styling not present in S5, Chrome - */ -dfn { - font-style: italic; -} - -/* - * Addresses styling not present in IE6/7/8/9 - */ -mark { - background: #ff0; - color: #000; -} - -/* - * Addresses margins set differently in IE6/7 - */ -p, -pre { - margin: 1em 0; -} - -/* - * Corrects font family set oddly in IE6, S4/5, Chrome - * en.wikipedia.org/wiki/User:Davidgothberg/Test59 - */ -pre, -code, -kbd, -samp { - font-family: monospace, serif; - _font-family: 'courier new', monospace; - font-size: 1em; -} - -/* - * 1. Addresses CSS quotes not supported in IE6/7 - * 2. Addresses quote property not supported in S4 - */ -/* 1 */ -q { - quotes: none; -} - -/* 2 */ -q:before, -q:after { - content: ''; - content: none; -} - -small { - font-size: 75%; -} - -/* - * Prevents sub and sup affecting line-height in all browsers - * gist.github.com/413930 - */ -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sup { - top: -0.5em; -} - -sub { - bottom: -0.25em; -} - -/* ============================================================================= - Lists - ========================================================================== */ -/* - * Addresses margins set differently in IE6/7 - */ -dl, -menu, -ol, -ul { - margin: 1em 0; -} - -dd { - margin: 0 0 0 40px; -} - -/* - * Addresses paddings set differently in IE6/7 - */ -menu, -ol, -ul { - padding: 0 0 0 40px; -} - -/* - * Corrects list images handled incorrectly in IE7 - */ -nav ul, -nav ol { - list-style: none; - list-style-image: none; -} - -/* ============================================================================= - Embedded content - ========================================================================== */ -/* - * 1. Removes border when inside 'a' element in IE6/7/8/9, FF3 - * 2. Improves image quality when scaled in IE7 - * code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/ - */ -img { - border: 0; - /* 1 */ - -ms-interpolation-mode: bicubic; - /* 2 */ -} - -/* - * Corrects overflow displayed oddly in IE9 - */ -svg:not(:root) { - overflow: hidden; -} - -/* ============================================================================= - Figures - ========================================================================== */ -/* - * Addresses margin not present in IE6/7/8/9, S5, O11 - */ -figure { - margin: 0; -} - -/* ============================================================================= - Forms - ========================================================================== */ -/* - * Corrects margin displayed oddly in IE6/7 - */ -form { - margin: 0; -} - -/* - * Define consistent border, margin, and padding - */ -fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; -} - -/* - * 1. Corrects color not being inherited in IE6/7/8/9 - * 2. Corrects text not wrapping in FF3 - * 3. Corrects alignment displayed oddly in IE6/7 - */ -legend { - border: 0; - /* 1 */ - padding: 0; - white-space: normal; - /* 2 */ - *margin-left: -7px; - /* 3 */ -} - -/* - * 1. Corrects font size not being inherited in all browsers - * 2. Addresses margins set differently in IE6/7, FF3+, S5, Chrome - * 3. Improves appearance and consistency in all browsers - */ -button, -input, -select, -textarea { - font-size: 100%; - /* 1 */ - margin: 0; - /* 2 */ - vertical-align: baseline; - /* 3 */ - *vertical-align: middle; - /* 3 */ -} - -/* - * Addresses FF3/4 setting line-height on 'input' using !important in the UA stylesheet - */ -button, -input { - line-height: normal; - /* 1 */ -} - -/* - * 1. Improves usability and consistency of cursor style between image-type 'input' and others - * 2. Corrects inability to style clickable 'input' types in iOS - * 3. Removes inner spacing in IE7 without affecting normal text inputs - * Known issue: inner spacing remains in IE6 - */ -button, -input[type="button"], -input[type="reset"], -input[type="submit"] { - cursor: pointer; - /* 1 */ - -webkit-appearance: button; - /* 2 */ - *overflow: visible; - /* 3 */ -} - -/* - * Re-set default cursor for disabled elements - */ -button[disabled], -input[disabled] { - cursor: default; -} - -/* - * 1. Addresses box sizing set to content-box in IE8/9 - * 2. Removes excess padding in IE8/9 - * 3. Removes excess padding in IE7 - Known issue: excess padding remains in IE6 - */ -input[type="checkbox"], -input[type="radio"] { - box-sizing: border-box; - /* 1 */ - padding: 0; - /* 2 */ - *height: 13px; - /* 3 */ - *width: 13px; - /* 3 */ -} - -/* - * 1. Addresses appearance set to searchfield in S5, Chrome - * 2. Addresses box-sizing set to border-box in S5, Chrome (include -moz to future-proof) - */ -input[type="search"] { - -webkit-appearance: textfield; - /* 1 */ - -moz-box-sizing: content-box; - -webkit-box-sizing: content-box; - /* 2 */ - box-sizing: content-box; -} - -/* - * Removes inner padding and search cancel button in S5, Chrome on OS X - */ -input[type="search"]::-webkit-search-decoration, -input[type="search"]::-webkit-search-cancel-button { - -webkit-appearance: none; -} - -/* - * Removes inner padding and border in FF3+ - * www.sitepen.com/blog/2008/05/14/the-devils-in-the-details-fixing-dojos-toolbar-buttons/ - */ -button::-moz-focus-inner, -input::-moz-focus-inner { - border: 0; - padding: 0; -} - -/* - * 1. Removes default vertical scrollbar in IE6/7/8/9 - * 2. Improves readability and alignment in all browsers - */ -textarea { - overflow: auto; - /* 1 */ - vertical-align: top; - /* 2 */ -} - -/* ============================================================================= - Tables - ========================================================================== */ -/* - * Remove most spacing between table cells - */ -table { - border-collapse: collapse; - border-spacing: 0; -} diff --git a/stylesheets/styles.css b/stylesheets/styles.css deleted file mode 100644 index 9f6e68e8..00000000 --- a/stylesheets/styles.css +++ /dev/null @@ -1,851 +0,0 @@ -@font-face { - font-family: 'OpenSansLight'; - src: url("http://github.com/fonts/OpenSans-Light-webfont.eot"); - src: url("http://github.com/fonts/OpenSans-Light-webfont.eot?#iefix") format("embedded-opentype"), url("http://github.com/fonts/OpenSans-Light-webfont.woff") format("woff"), url("http://github.com/fonts/OpenSans-Light-webfont.ttf") format("truetype"), url("http://github.com/fonts/OpenSans-Light-webfont.svg#OpenSansLight") format("svg"); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'OpenSansLightItalic'; - src: url("http://github.com/fonts/OpenSans-LightItalic-webfont.eot"); - src: url("http://github.com/fonts/OpenSans-LightItalic-webfont.eot?#iefix") format("embedded-opentype"), url("http://github.com/fonts/OpenSans-LightItalic-webfont.woff") format("woff"), url("http://github.com/fonts/OpenSans-LightItalic-webfont.ttf") format("truetype"), url("http://github.com/fonts/OpenSans-LightItalic-webfont.svg#OpenSansLightItalic") format("svg"); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'OpenSansRegular'; - src: url("http://github.com/fonts/OpenSans-Regular-webfont.eot"); - src: url("http://github.com/fonts/OpenSans-Regular-webfont.eot?#iefix") format("embedded-opentype"), url("http://github.com/fonts/OpenSans-Regular-webfont.woff") format("woff"), url("http://github.com/fonts/OpenSans-Regular-webfont.ttf") format("truetype"), url("http://github.com/fonts/OpenSans-Regular-webfont.svg#OpenSansRegular") format("svg"); - font-weight: normal; - font-style: normal; - -webkit-font-smoothing: antialiased; -} - -@font-face { - font-family: 'OpenSansItalic'; - src: url("http://github.com/fonts/OpenSans-Italic-webfont.eot"); - src: url("http://github.com/fonts/OpenSans-Italic-webfont.eot?#iefix") format("embedded-opentype"), url("http://github.com/fonts/OpenSans-Italic-webfont.woff") format("woff"), url("http://github.com/fonts/OpenSans-Italic-webfont.ttf") format("truetype"), url("http://github.com/fonts/OpenSans-Italic-webfont.svg#OpenSansItalic") format("svg"); - font-weight: normal; - font-style: normal; - -webkit-font-smoothing: antialiased; -} - -@font-face { - font-family: 'OpenSansSemibold'; - src: url("http://github.com/fonts/OpenSans-Semibold-webfont.eot"); - src: url("http://github.com/fonts/OpenSans-Semibold-webfont.eot?#iefix") format("embedded-opentype"), url("http://github.com/fonts/OpenSans-Semibold-webfont.woff") format("woff"), url("http://github.com/fonts/OpenSans-Semibold-webfont.ttf") format("truetype"), url("http://github.com/fonts/OpenSans-Semibold-webfont.svg#OpenSansSemibold") format("svg"); - font-weight: normal; - font-style: normal; - -webkit-font-smoothing: antialiased; -} - -@font-face { - font-family: 'OpenSansSemiboldItalic'; - src: url("http://github.com/fonts/OpenSans-SemiboldItalic-webfont.eot"); - src: url("http://github.com/fonts/OpenSans-SemiboldItalic-webfont.eot?#iefix") format("embedded-opentype"), url("http://github.com/fonts/OpenSans-SemiboldItalic-webfont.woff") format("woff"), url("http://github.com/fonts/OpenSans-SemiboldItalic-webfont.ttf") format("truetype"), url("http://github.com/fonts/OpenSans-SemiboldItalic-webfont.svg#OpenSansSemiboldItalic") format("svg"); - font-weight: normal; - font-style: normal; - -webkit-font-smoothing: antialiased; -} - -@font-face { - font-family: 'OpenSansBold'; - src: url("http://github.com/fonts/OpenSans-Bold-webfont.eot"); - src: url("http://github.com/fonts/OpenSans-Bold-webfont.eot?#iefix") format("embedded-opentype"), url("http://github.com/fonts/OpenSans-Bold-webfont.woff") format("woff"), url("http://github.com/fonts/OpenSans-Bold-webfont.ttf") format("truetype"), url("http://github.com/fonts/OpenSans-Bold-webfont.svg#OpenSansBold") format("svg"); - font-weight: normal; - font-style: normal; - -webkit-font-smoothing: antialiased; -} - -@font-face { - font-family: 'OpenSansBoldItalic'; - src: url("http://github.com/fonts/OpenSans-BoldItalic-webfont.eot"); - src: url("http://github.com/fonts/OpenSans-BoldItalic-webfont.eot?#iefix") format("embedded-opentype"), url("http://github.com/fonts/OpenSans-BoldItalic-webfont.woff") format("woff"), url("http://github.com/fonts/OpenSans-BoldItalic-webfont.ttf") format("truetype"), url("http://github.com/fonts/OpenSans-BoldItalic-webfont.svg#OpenSansBoldItalic") format("svg"); - font-weight: normal; - font-style: normal; - -webkit-font-smoothing: antialiased; -} - -/* normalize.css 2012-02-07T12:37 UTC - https://github.com/necolas/normalize.css */ -/* ============================================================================= - HTML5 display definitions - ========================================================================== */ -/* - * Corrects block display not defined in IE6/7/8/9 & FF3 - */ -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -nav, -section, -summary { - display: block; -} - -/* - * Corrects inline-block display not defined in IE6/7/8/9 & FF3 - */ -audio, -canvas, -video { - display: inline-block; - *display: inline; - *zoom: 1; -} - -/* - * Prevents modern browsers from displaying 'audio' without controls - */ -audio:not([controls]) { - display: none; -} - -/* - * Addresses styling for 'hidden' attribute not present in IE7/8/9, FF3, S4 - * Known issue: no IE6 support - */ -[hidden] { - display: none; -} - -/* ============================================================================= - Base - ========================================================================== */ -/* - * 1. Corrects text resizing oddly in IE6/7 when body font-size is set using em units - * http://clagnut.com/blog/348/#c790 - * 2. Prevents iOS text size adjust after orientation change, without disabling user zoom - * www.456bereastreet.com/archive/201012/controlling_text_size_in_safari_for_ios_without_disabling_user_zoom/ - */ -html { - font-size: 100%; - /* 1 */ - -webkit-text-size-adjust: 100%; - /* 2 */ - -ms-text-size-adjust: 100%; - /* 2 */ -} - -/* - * Addresses font-family inconsistency between 'textarea' and other form elements. - */ -html, -button, -input, -select, -textarea { - font-family: sans-serif; -} - -/* - * Addresses margins handled incorrectly in IE6/7 - */ -body { - margin: 0; -} - -/* ============================================================================= - Links - ========================================================================== */ -/* - * Addresses outline displayed oddly in Chrome - */ -a:focus { - outline: thin dotted; -} - -/* - * Improves readability when focused and also mouse hovered in all browsers - * people.opera.com/patrickl/experiments/keyboard/test - */ -a:hover, -a:active { - outline: 0; -} - -/* ============================================================================= - Typography - ========================================================================== */ -/* - * Addresses font sizes and margins set differently in IE6/7 - * Addresses font sizes within 'section' and 'article' in FF4+, Chrome, S5 - */ -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -h2 { - font-size: 1.5em; - margin: 0.83em 0; -} - -h3 { - font-size: 1.17em; - margin: 1em 0; -} - -h4 { - font-size: 1em; - margin: 1.33em 0; -} - -h5 { - font-size: 0.83em; - margin: 1.67em 0; -} - -h6 { - font-size: 0.75em; - margin: 2.33em 0; -} - -/* - * Addresses styling not present in IE7/8/9, S5, Chrome - */ -abbr[title] { - border-bottom: 1px dotted; -} - -/* - * Addresses style set to 'bolder' in FF3+, S4/5, Chrome -*/ -b, -strong { - font-weight: bold; -} - -blockquote { - margin: 1em 40px; -} - -/* - * Addresses styling not present in S5, Chrome - */ -dfn { - font-style: italic; -} - -/* - * Addresses styling not present in IE6/7/8/9 - */ -mark { - background: #ff0; - color: #000; -} - -/* - * Addresses margins set differently in IE6/7 - */ -p, -pre { - margin: 1em 0; -} - -/* - * Corrects font family set oddly in IE6, S4/5, Chrome - * en.wikipedia.org/wiki/User:Davidgothberg/Test59 - */ -pre, -code, -kbd, -samp { - font-family: monospace, serif; - _font-family: 'courier new', monospace; - font-size: 1em; -} - -/* - * 1. Addresses CSS quotes not supported in IE6/7 - * 2. Addresses quote property not supported in S4 - */ -/* 1 */ -q { - quotes: none; -} - -/* 2 */ -q:before, -q:after { - content: ''; - content: none; -} - -small { - font-size: 75%; -} - -/* - * Prevents sub and sup affecting line-height in all browsers - * gist.github.com/413930 - */ -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sup { - top: -0.5em; -} - -sub { - bottom: -0.25em; -} - -/* ============================================================================= - Lists - ========================================================================== */ -/* - * Addresses margins set differently in IE6/7 - */ -dl, -menu, -ol, -ul { - margin: 1em 0; -} - -dd { - margin: 0 0 0 40px; -} - -/* - * Addresses paddings set differently in IE6/7 - */ -menu, -ol, -ul { - padding: 0 0 0 40px; -} - -/* - * Corrects list images handled incorrectly in IE7 - */ -nav ul, -nav ol { - list-style: none; - list-style-image: none; -} - -/* ============================================================================= - Embedded content - ========================================================================== */ -/* - * 1. Removes border when inside 'a' element in IE6/7/8/9, FF3 - * 2. Improves image quality when scaled in IE7 - * code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/ - */ -img { - border: 0; - /* 1 */ - -ms-interpolation-mode: bicubic; - /* 2 */ -} - -/* - * Corrects overflow displayed oddly in IE9 - */ -svg:not(:root) { - overflow: hidden; -} - -/* ============================================================================= - Figures - ========================================================================== */ -/* - * Addresses margin not present in IE6/7/8/9, S5, O11 - */ -figure { - margin: 0; -} - -/* ============================================================================= - Forms - ========================================================================== */ -/* - * Corrects margin displayed oddly in IE6/7 - */ -form { - margin: 0; -} - -/* - * Define consistent border, margin, and padding - */ -fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; -} - -/* - * 1. Corrects color not being inherited in IE6/7/8/9 - * 2. Corrects text not wrapping in FF3 - * 3. Corrects alignment displayed oddly in IE6/7 - */ -legend { - border: 0; - /* 1 */ - padding: 0; - white-space: normal; - /* 2 */ - *margin-left: -7px; - /* 3 */ -} - -/* - * 1. Corrects font size not being inherited in all browsers - * 2. Addresses margins set differently in IE6/7, FF3+, S5, Chrome - * 3. Improves appearance and consistency in all browsers - */ -button, -input, -select, -textarea { - font-size: 100%; - /* 1 */ - margin: 0; - /* 2 */ - vertical-align: baseline; - /* 3 */ - *vertical-align: middle; - /* 3 */ -} - -/* - * Addresses FF3/4 setting line-height on 'input' using !important in the UA stylesheet - */ -button, -input { - line-height: normal; - /* 1 */ -} - -/* - * 1. Improves usability and consistency of cursor style between image-type 'input' and others - * 2. Corrects inability to style clickable 'input' types in iOS - * 3. Removes inner spacing in IE7 without affecting normal text inputs - * Known issue: inner spacing remains in IE6 - */ -button, -input[type="button"], -input[type="reset"], -input[type="submit"] { - cursor: pointer; - /* 1 */ - -webkit-appearance: button; - /* 2 */ - *overflow: visible; - /* 3 */ -} - -/* - * Re-set default cursor for disabled elements - */ -button[disabled], -input[disabled] { - cursor: default; -} - -/* - * 1. Addresses box sizing set to content-box in IE8/9 - * 2. Removes excess padding in IE8/9 - * 3. Removes excess padding in IE7 - Known issue: excess padding remains in IE6 - */ -input[type="checkbox"], -input[type="radio"] { - box-sizing: border-box; - /* 1 */ - padding: 0; - /* 2 */ - *height: 13px; - /* 3 */ - *width: 13px; - /* 3 */ -} - -/* - * 1. Addresses appearance set to searchfield in S5, Chrome - * 2. Addresses box-sizing set to border-box in S5, Chrome (include -moz to future-proof) - */ -input[type="search"] { - -webkit-appearance: textfield; - /* 1 */ - -moz-box-sizing: content-box; - -webkit-box-sizing: content-box; - /* 2 */ - box-sizing: content-box; -} - -/* - * Removes inner padding and search cancel button in S5, Chrome on OS X - */ -input[type="search"]::-webkit-search-decoration, -input[type="search"]::-webkit-search-cancel-button { - -webkit-appearance: none; -} - -/* - * Removes inner padding and border in FF3+ - * www.sitepen.com/blog/2008/05/14/the-devils-in-the-details-fixing-dojos-toolbar-buttons/ - */ -button::-moz-focus-inner, -input::-moz-focus-inner { - border: 0; - padding: 0; -} - -/* - * 1. Removes default vertical scrollbar in IE6/7/8/9 - * 2. Improves readability and alignment in all browsers - */ -textarea { - overflow: auto; - /* 1 */ - vertical-align: top; - /* 2 */ -} - -/* ============================================================================= - Tables - ========================================================================== */ -/* - * Remove most spacing between table cells - */ -table { - border-collapse: collapse; - border-spacing: 0; -} - -body { - padding: 0px 0 20px 0px; - margin: 0px; - font: 14px/1.5 "OpenSansRegular", "Helvetica Neue", Helvetica, Arial, sans-serif; - color: #f0e7d5; - font-weight: normal; - background: #252525; - background-attachment: fixed !important; - background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #2a2a29), color-stop(100%, #1c1c1c)); - background: -webkit-linear-gradient(#2a2a29, #1c1c1c); - background: -moz-linear-gradient(#2a2a29, #1c1c1c); - background: -o-linear-gradient(#2a2a29, #1c1c1c); - background: -ms-linear-gradient(#2a2a29, #1c1c1c); - background: linear-gradient(#2a2a29, #1c1c1c); -} - -h1, h2, h3, h4, h5, h6 { - color: #e8e8e8; - margin: 0 0 10px; - font-family: 'OpenSansRegular', "Helvetica Neue", Helvetica, Arial, sans-serif; - font-weight: normal; -} - -p, ul, ol, table, pre, dl { - margin: 0 0 20px; -} - -h1, h2, h3 { - line-height: 1.1; -} - -h1 { - font-size: 28px; -} - -h2 { - font-size: 24px; -} - -h4, h5, h6 { - color: #e8e8e8; -} - -h3 { - font-size: 18px; - line-height: 24px; - font-family: 'OpenSansRegular', "Helvetica Neue", Helvetica, Arial, sans-serif !important; - font-weight: normal; - color: #b6b6b6; -} - -a { - color: #ffcc00; - font-weight: 400; - text-decoration: none; -} -a:hover { - color: #ffeb9b; -} - -a small { - font-size: 11px; - color: #666; - margin-top: -0.6em; - display: block; -} - -ul { - list-style-image: url("http://github.com/images/bullet.png"); -} - -strong { - font-family: 'OpenSansBold', "Helvetica Neue", Helvetica, Arial, sans-serif !important; - font-weight: normal; -} - -.wrapper { - max-width: 650px; - margin: 0 auto; - position: relative; - padding: 0 20px; -} - -section img { - max-width: 100%; -} - -blockquote { - border-left: 3px solid #ffcc00; - margin: 0; - padding: 0 0 0 20px; - font-style: italic; -} - -code { - font-family: "Lucida Sans", Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal; - color: #efefef; - font-size: 13px; - margin: 0 4px; - padding: 4px 6px; - -moz-border-radius: 2px; - -webkit-border-radius: 2px; - -o-border-radius: 2px; - -ms-border-radius: 2px; - -khtml-border-radius: 2px; - border-radius: 2px; -} - -pre { - padding: 8px 15px; - background: #191919; - -moz-border-radius: 2px; - -webkit-border-radius: 2px; - -o-border-radius: 2px; - -ms-border-radius: 2px; - -khtml-border-radius: 2px; - border-radius: 2px; - border: 1px solid #121212; - -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3); - -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3); - -o-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3); - box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3); - overflow: auto; - overflow-y: hidden; -} -pre code { - color: #efefef; - text-shadow: 0px 1px 0px #000; - margin: 0; - padding: 0; -} - -table { - width: 100%; - border-collapse: collapse; -} - -th { - text-align: left; - padding: 5px 10px; - border-bottom: 1px solid #434343; - color: #b6b6b6; - font-family: 'OpenSansSemibold', "Helvetica Neue", Helvetica, Arial, sans-serif !important; - font-weight: normal; -} - -td { - text-align: left; - padding: 5px 10px; - border-bottom: 1px solid #434343; -} - -hr { - border: 0; - outline: none; - height: 3px; - background: transparent url("http://github.com/images/hr.gif") center center repeat-x; - margin: 0 0 20px; -} - -dt { - color: #F0E7D5; - font-family: 'OpenSansSemibold', "Helvetica Neue", Helvetica, Arial, sans-serif !important; - font-weight: normal; -} - -#header { - z-index: 100; - left: 0; - top: 0px; - height: 60px; - width: 100%; - position: fixed; - background: url(http://github.com/images/nav-bg.gif) #353535; - border-bottom: 4px solid #434343; - -moz-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.25); - -webkit-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.25); - -o-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.25); - box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.25); -} -#header nav { - max-width: 650px; - margin: 0 auto; - padding: 0 10px; - background: blue; - margin: 6px auto; -} -#header nav li { - font-family: 'OpenSansLight', "Helvetica Neue", Helvetica, Arial, sans-serif; - font-weight: normal; - list-style: none; - display: inline; - color: white; - line-height: 50px; - text-shadow: 0px 1px 0px rgba(0, 0, 0, 0.2); - font-size: 14px; -} -#header nav li a { - color: white; - border: 1px solid #5d910b; - background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #93bd20), color-stop(100%, #659e10)); - background: -webkit-linear-gradient(#93bd20, #659e10); - background: -moz-linear-gradient(#93bd20, #659e10); - background: -o-linear-gradient(#93bd20, #659e10); - background: -ms-linear-gradient(#93bd20, #659e10); - background: linear-gradient(#93bd20, #659e10); - -moz-border-radius: 2px; - -webkit-border-radius: 2px; - -o-border-radius: 2px; - -ms-border-radius: 2px; - -khtml-border-radius: 2px; - border-radius: 2px; - -moz-box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.3), 0px 3px 7px rgba(0, 0, 0, 0.7); - -webkit-box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.3), 0px 3px 7px rgba(0, 0, 0, 0.7); - -o-box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.3), 0px 3px 7px rgba(0, 0, 0, 0.7); - box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.3), 0px 3px 7px rgba(0, 0, 0, 0.7); - background-color: #93bd20; - padding: 10px 12px; - margin-top: 6px; - line-height: 14px; - font-size: 14px; - display: inline-block; - text-align: center; -} -#header nav li a:hover { - background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #749619), color-stop(100%, #527f0e)); - background: -webkit-linear-gradient(#749619, #527f0e); - background: -moz-linear-gradient(#749619, #527f0e); - background: -o-linear-gradient(#749619, #527f0e); - background: -ms-linear-gradient(#749619, #527f0e); - background: linear-gradient(#749619, #527f0e); - background-color: #659e10; - border: 1px solid #527f0e; - -moz-box-shadow: inset 0px 1px 1px rgba(0, 0, 0, 0.2), 0px 1px 0px rgba(0, 0, 0, 0); - -webkit-box-shadow: inset 0px 1px 1px rgba(0, 0, 0, 0.2), 0px 1px 0px rgba(0, 0, 0, 0); - -o-box-shadow: inset 0px 1px 1px rgba(0, 0, 0, 0.2), 0px 1px 0px rgba(0, 0, 0, 0); - box-shadow: inset 0px 1px 1px rgba(0, 0, 0, 0.2), 0px 1px 0px rgba(0, 0, 0, 0); -} -#header nav li.fork { - float: left; - margin-left: 0px; -} -#header nav li.downloads { - float: right; - margin-left: 6px; -} -#header nav li.title { - float: right; - margin-right: 10px; - font-size: 11px; -} - -section { - max-width: 650px; - padding: 30px 0px 50px 0px; - margin: 20px 0; - margin-top: 70px; -} -section #title { - border: 0; - outline: none; - margin: 0 0 50px 0; - padding: 0 0 5px 0; -} -section #title h1 { - font-family: 'OpenSansLight', "Helvetica Neue", Helvetica, Arial, sans-serif; - font-weight: normal; - font-size: 40px; - text-align: center; - line-height: 36px; -} -section #title p { - color: #d7cfbe; - font-family: 'OpenSansLight', "Helvetica Neue", Helvetica, Arial, sans-serif; - font-weight: normal; - font-size: 18px; - text-align: center; -} -section #title .credits { - font-size: 11px; - font-family: 'OpenSansRegular', "Helvetica Neue", Helvetica, Arial, sans-serif; - font-weight: normal; - color: #696969; - margin-top: -10px; -} -section #title .credits.left { - float: left; -} -section #title .credits.right { - float: right; -} - -@media print, screen and (max-width: 720px) { - #title .credits { - display: block; - width: 100%; - line-height: 30px; - text-align: center; - } - #title .credits .left { - float: none; - display: block; - } - #title .credits .right { - float: none; - display: block; - } -} -@media print, screen and (max-width: 480px) { - #header { - margin-top: -20px; - } - - section { - margin-top: 40px; - } - - nav { - display: none; - } -} 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/tests/redmine-net-api.Tests/Infrastructure/Order/CaseOrder.cs b/tests/redmine-net-api.Tests/Infrastructure/Order/CaseOrder.cs new file mode 100644 index 00000000..b0f8f375 --- /dev/null +++ b/tests/redmine-net-api.Tests/Infrastructure/Order/CaseOrder.cs @@ -0,0 +1,42 @@ +#if !(NET20 || NET40) +using System.Collections.Concurrent; +using System.Reflection; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure.Order +{ + /// + /// Custom xUnit test case orderer that uses the OrderAttribute + /// + public sealed class CaseOrderer : ITestCaseOrderer + { + // public const string TYPE_NAME = "redmine.net.api.Tests.Infrastructure.CaseOrderer"; + // public const string ASSEMBLY_NAME = "redmine-net-api.Tests"; + + private static readonly ConcurrentDictionary> QueuedTests = new ConcurrentDictionary>(); + + public IEnumerable OrderTestCases(IEnumerable testCases) + where TTestCase : ITestCase + { + return testCases.OrderBy(GetOrder); + } + + private static int GetOrder(TTestCase testCase) + where TTestCase : ITestCase + { + // Enqueue the test name. + QueuedTests + .GetOrAdd(testCase.TestMethod.TestClass.Class.Name,key => new ConcurrentQueue()) + .Enqueue(testCase.TestMethod.Method.Name); + + // Order the test based on the attribute. + var attr = testCase.TestMethod.Method + .ToRuntimeMethod() + .GetCustomAttribute(); + + return attr?.Index ?? 0; + } + } +} +#endif \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Infrastructure/Order/CollectionOrderer.cs b/tests/redmine-net-api.Tests/Infrastructure/Order/CollectionOrderer.cs new file mode 100644 index 00000000..b1ad5a08 --- /dev/null +++ b/tests/redmine-net-api.Tests/Infrastructure/Order/CollectionOrderer.cs @@ -0,0 +1,48 @@ +#if !(NET20 || NET40) + +using System.Reflection; +using Xunit; +using Xunit.Abstractions; + +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure.Order +{ + /// + /// Custom xUnit test collection orderer that uses the OrderAttribute + /// + public sealed class CollectionOrderer : ITestCollectionOrderer + { + // 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) + { + return testCollections.OrderBy(GetOrder); + } + + /// + /// Test collections are not bound to a specific class, however they + /// are named by default with the type name as a suffix. We try to + /// get the class name from the DisplayName and then use reflection to + /// find the class and OrderAttribute. + /// + private static int GetOrder(ITestCollection testCollection) + { + var index = testCollection.DisplayName.LastIndexOf(' '); + if (index <= -1) + { + return 0; + } + + var className = testCollection.DisplayName.Substring(index + 1); + var type = Type.GetType(className); + if (type == null) + { + return 0; + } + + var attr = type.GetCustomAttribute(); + return attr?.Index ?? 0; + } + } +} +#endif \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Infrastructure/Order/OrderAttribute.cs b/tests/redmine-net-api.Tests/Infrastructure/Order/OrderAttribute.cs new file mode 100644 index 00000000..1c7e08e7 --- /dev/null +++ b/tests/redmine-net-api.Tests/Infrastructure/Order/OrderAttribute.cs @@ -0,0 +1,12 @@ +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure.Order +{ + public sealed class OrderAttribute : Attribute + { + public OrderAttribute(int index) + { + Index = index; + } + + public int Index { get; private set; } + } +} \ No newline at end of file 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