From 75a687e31ec18d42de82af358968f003032a4720 Mon Sep 17 00:00:00 2001 From: Matthias Langhard Date: Mon, 7 Dec 2020 22:00:24 +0100 Subject: [PATCH] chore: initial commit --- .gitignore | 117 ++++++++++++++++++ .gitlab-ci.yml | 18 +++ Novaloop.PaymoApi.sln | 48 +++++++ README.md | 50 ++++++++ pack.sh | 2 + src/Exceptions/PaymoApiException.cs | 14 +++ src/Extensions/HttpClientExtensions.cs | 37 ++++++ .../HttpResponseMessageExtensions.cs | 17 +++ src/Extensions/PaymoApiExtensions.cs | 20 +++ src/Extensions/PaymoApiOptions.cs | 8 ++ src/Novaloop.PaymoApi.csproj | 19 +++ src/Shared/PaymoApiClient.cs | 14 +++ src/Tasks/IPaymoTasksApiService.cs | 11 ++ src/Tasks/Models/CreateTaskRequest.cs | 12 ++ src/Tasks/Models/CreateTaskResponse.cs | 7 ++ src/Tasks/Models/GetTasksResponse.cs | 9 ++ src/Tasks/Models/PaymoTask.cs | 25 ++++ src/Tasks/PaymoTasksApiService.cs | 44 +++++++ tests/Novaloop.PaymoApi.Tests.csproj | 22 ++++ tests/UnitTest1.cs | 13 ++ 20 files changed, 507 insertions(+) create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 Novaloop.PaymoApi.sln create mode 100644 README.md create mode 100755 pack.sh create mode 100644 src/Exceptions/PaymoApiException.cs create mode 100644 src/Extensions/HttpClientExtensions.cs create mode 100644 src/Extensions/HttpResponseMessageExtensions.cs create mode 100644 src/Extensions/PaymoApiExtensions.cs create mode 100644 src/Extensions/PaymoApiOptions.cs create mode 100644 src/Novaloop.PaymoApi.csproj create mode 100644 src/Shared/PaymoApiClient.cs create mode 100644 src/Tasks/IPaymoTasksApiService.cs create mode 100644 src/Tasks/Models/CreateTaskRequest.cs create mode 100644 src/Tasks/Models/CreateTaskResponse.cs create mode 100644 src/Tasks/Models/GetTasksResponse.cs create mode 100644 src/Tasks/Models/PaymoTask.cs create mode 100644 src/Tasks/PaymoTasksApiService.cs create mode 100644 tests/Novaloop.PaymoApi.Tests.csproj create mode 100644 tests/UnitTest1.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..28f8616 --- /dev/null +++ b/.gitignore @@ -0,0 +1,117 @@ + +# Created by https://www.gitignore.io/api/dotnetcore,jetbrains+all,visualstudiocode +# Edit at https://www.gitignore.io/?templates=dotnetcore,jetbrains+all,visualstudiocode + +### DotnetCore ### +# .NET Core build folders +/bin +/obj + +# Common node modules locations +/node_modules +/wwwroot/node_modules + + +### JetBrains+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### JetBrains+all Patch ### +# Ignores the whole .idea folder and all .iml files +# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 + +.idea/ + +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +# Sonarlint plugin +.idea/sonarlint + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history + +# End of https://www.gitignore.io/api/dotnetcore,jetbrains+all,visualstudiocode +publish.sh +tests/bin +tests/obj +src/obj +src/bin diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..f626432 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,18 @@ +stages: + - test + - publish + +running tests for tag: + image: mcr.microsoft.com/dotnet/core/sdk:3.1 + stage: test + script: + - dotnet test ./tests + +publish to nuget: + only: + - /^\d*.\d*.\d*$/ # gets triggered if the commit tag is in the form n.n.n where n is any number + image: mcr.microsoft.com/dotnet/core/sdk:3.1 + stage: publish + script: + - dotnet pack src -o ./packaged + - dotnet nuget push ./packaged/*.nupkg -k $NUGET_API_KEY -s https://api.nuget.org/v3/index.json diff --git a/Novaloop.PaymoApi.sln b/Novaloop.PaymoApi.sln new file mode 100644 index 0000000..60606a7 --- /dev/null +++ b/Novaloop.PaymoApi.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Novaloop.PaymoApi", "src\Novaloop.PaymoApi.csproj", "{A9612B7C-67C1-4B6B-8260-167079A31FAF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Novaloop.PaymoApi.Tests", "tests\Novaloop.PaymoApi.Tests.csproj", "{202BCB4F-78AF-4E9A-B286-C3147374EB53}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A9612B7C-67C1-4B6B-8260-167079A31FAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A9612B7C-67C1-4B6B-8260-167079A31FAF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A9612B7C-67C1-4B6B-8260-167079A31FAF}.Debug|x64.ActiveCfg = Debug|Any CPU + {A9612B7C-67C1-4B6B-8260-167079A31FAF}.Debug|x64.Build.0 = Debug|Any CPU + {A9612B7C-67C1-4B6B-8260-167079A31FAF}.Debug|x86.ActiveCfg = Debug|Any CPU + {A9612B7C-67C1-4B6B-8260-167079A31FAF}.Debug|x86.Build.0 = Debug|Any CPU + {A9612B7C-67C1-4B6B-8260-167079A31FAF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A9612B7C-67C1-4B6B-8260-167079A31FAF}.Release|Any CPU.Build.0 = Release|Any CPU + {A9612B7C-67C1-4B6B-8260-167079A31FAF}.Release|x64.ActiveCfg = Release|Any CPU + {A9612B7C-67C1-4B6B-8260-167079A31FAF}.Release|x64.Build.0 = Release|Any CPU + {A9612B7C-67C1-4B6B-8260-167079A31FAF}.Release|x86.ActiveCfg = Release|Any CPU + {A9612B7C-67C1-4B6B-8260-167079A31FAF}.Release|x86.Build.0 = Release|Any CPU + {202BCB4F-78AF-4E9A-B286-C3147374EB53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {202BCB4F-78AF-4E9A-B286-C3147374EB53}.Debug|Any CPU.Build.0 = Debug|Any CPU + {202BCB4F-78AF-4E9A-B286-C3147374EB53}.Debug|x64.ActiveCfg = Debug|Any CPU + {202BCB4F-78AF-4E9A-B286-C3147374EB53}.Debug|x64.Build.0 = Debug|Any CPU + {202BCB4F-78AF-4E9A-B286-C3147374EB53}.Debug|x86.ActiveCfg = Debug|Any CPU + {202BCB4F-78AF-4E9A-B286-C3147374EB53}.Debug|x86.Build.0 = Debug|Any CPU + {202BCB4F-78AF-4E9A-B286-C3147374EB53}.Release|Any CPU.ActiveCfg = Release|Any CPU + {202BCB4F-78AF-4E9A-B286-C3147374EB53}.Release|Any CPU.Build.0 = Release|Any CPU + {202BCB4F-78AF-4E9A-B286-C3147374EB53}.Release|x64.ActiveCfg = Release|Any CPU + {202BCB4F-78AF-4E9A-B286-C3147374EB53}.Release|x64.Build.0 = Release|Any CPU + {202BCB4F-78AF-4E9A-B286-C3147374EB53}.Release|x86.ActiveCfg = Release|Any CPU + {202BCB4F-78AF-4E9A-B286-C3147374EB53}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md new file mode 100644 index 0000000..2a0827b --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# Novaloop.PaymoApi - Accessing Paymo Api + +**Novaloop.PaymoApi** allows to access the paymo API. + +## Implemented Methods + +- Get an existing Task +- Create a new Task + +## Getting Started + +### Startup.cs + +The api client is added with the following configuration inside `ConfigureServices`. +See https://github.com/paymoapp/api/blob/master/sections/authentication.md#using-sessions for how to acquire an api key. + +```csharp +public void ConfigureServices(IServiceCollection services) +{ + services.AddPaymoApi(options => + { + options.ApiKey = "your-api-key"; + }); + services.AddControllers(); +} +``` + +### Usage + +Now the Api Services can be injected via dependency injection inside controllers / services: + +```csharp +[ApiController] +[Route("[controller]")] +public class ExampleController : ControllerBase +{ + private readonly IPaymoTaskApiService _paymoTaskService; + + public ExampleController(IPaymoTaskApiService paymoTaskService) + { + _paymoTaskService = paymoTaskService; + } + + [HttpGet] + public async Task Get() + { + return Ok(await _paymoTaskService.GetTask(11)); + } +} +``` diff --git a/pack.sh b/pack.sh new file mode 100755 index 0000000..ef11f48 --- /dev/null +++ b/pack.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +dotnet pack src -o ../local-nuget-packages \ No newline at end of file diff --git a/src/Exceptions/PaymoApiException.cs b/src/Exceptions/PaymoApiException.cs new file mode 100644 index 0000000..f0f7fb8 --- /dev/null +++ b/src/Exceptions/PaymoApiException.cs @@ -0,0 +1,14 @@ +using System; + +namespace Novaloop.PaymoApi.Exceptions +{ + public class PaymoApiException : Exception + { + public PaymoApiException(int statusCode, string message) : base($"[{statusCode}]: {message})") + { + StatusCode = statusCode; + } + + public int StatusCode { get; } + } +} \ No newline at end of file diff --git a/src/Extensions/HttpClientExtensions.cs b/src/Extensions/HttpClientExtensions.cs new file mode 100644 index 0000000..eb554b7 --- /dev/null +++ b/src/Extensions/HttpClientExtensions.cs @@ -0,0 +1,37 @@ +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; + +namespace Novaloop.PaymoApi.Extensions +{ + internal static class HttpClientExtensions + { + internal static void SetApiKeyHeader(this HttpClient client, string apiKey) + { + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Base64Encode($"{apiKey}:random")); + } + + internal static async Task PatchAsync(this HttpClient client, string uri, HttpContent content) + { + var method = new HttpMethod("PATCH"); + if (client.BaseAddress is null) + { + throw new ArgumentException("Can not handle 'BaseAddress' null value configuration."); + } + + var request = new HttpRequestMessage(method, new Uri(CombineBaseUrlWithSegment(client.BaseAddress.ToString(), uri))) {Content = content}; + return await client.SendAsync(request); + } + + private static string CombineBaseUrlWithSegment(string uri1, string uri2) + { + return $"{uri1.TrimEnd('/')}/{uri2.TrimStart('/')}"; + } + + private static string Base64Encode(string plainText) + { + return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(plainText)); + } + } +} \ No newline at end of file diff --git a/src/Extensions/HttpResponseMessageExtensions.cs b/src/Extensions/HttpResponseMessageExtensions.cs new file mode 100644 index 0000000..84cf220 --- /dev/null +++ b/src/Extensions/HttpResponseMessageExtensions.cs @@ -0,0 +1,17 @@ +using System.Net.Http; +using System.Threading.Tasks; +using Novaloop.PaymoApi.Exceptions; + +namespace Novaloop.PaymoApi.Extensions +{ + internal static class HttpResponseMessageExtensions + { + internal static async Task ThrowExceptionWithDetailsIfUnsuccessful(this HttpResponseMessage response) + { + if (!response.IsSuccessStatusCode) + { + throw new PaymoApiException((int) response.StatusCode, await response.Content.ReadAsStringAsync()); + } + } + } +} \ No newline at end of file diff --git a/src/Extensions/PaymoApiExtensions.cs b/src/Extensions/PaymoApiExtensions.cs new file mode 100644 index 0000000..f535b47 --- /dev/null +++ b/src/Extensions/PaymoApiExtensions.cs @@ -0,0 +1,20 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Novaloop.PaymoApi.Shared; +using Novaloop.PaymoApi.Tasks; + +namespace Novaloop.PaymoApi.Extensions +{ + public static class PaymoApiExtensions + { + public static IServiceCollection AddPaymoApi(this IServiceCollection services, Action options) + { + services.Configure(options); + var resolvedOptions = (IOptions) services.BuildServiceProvider().GetService(typeof(IOptions)); + services.AddHttpClient(client => { client.BaseAddress = new Uri(resolvedOptions.Value.BaseUrl); }); + services.AddTransient(); + return services; + } + } +} \ No newline at end of file diff --git a/src/Extensions/PaymoApiOptions.cs b/src/Extensions/PaymoApiOptions.cs new file mode 100644 index 0000000..f56d6f0 --- /dev/null +++ b/src/Extensions/PaymoApiOptions.cs @@ -0,0 +1,8 @@ +namespace Novaloop.PaymoApi.Extensions +{ + public class PaymoApiOptions + { + public string BaseUrl { get; set; } = "https://app.paymoapp.com/api"; + public string ApiToken { get; set; } + } +} \ No newline at end of file diff --git a/src/Novaloop.PaymoApi.csproj b/src/Novaloop.PaymoApi.csproj new file mode 100644 index 0000000..3b1c8b5 --- /dev/null +++ b/src/Novaloop.PaymoApi.csproj @@ -0,0 +1,19 @@ + + + + netstandard2.0 + Novaloop.PaymoApi + Access your paymo instance for asp.net core + api;paymo;asp.net core; + 0.1.0 + Matthias Langhard + Novaloop AG + https://gitlab.com/novaloop-oss/novaloop.paymoapi + + + + + + + + diff --git a/src/Shared/PaymoApiClient.cs b/src/Shared/PaymoApiClient.cs new file mode 100644 index 0000000..c347a9e --- /dev/null +++ b/src/Shared/PaymoApiClient.cs @@ -0,0 +1,14 @@ +using System.Net.Http; + +namespace Novaloop.PaymoApi.Shared +{ + public class PaymoApiClient + { + public PaymoApiClient(HttpClient client) + { + Client = client; + } + + public HttpClient Client { get; } + } +} \ No newline at end of file diff --git a/src/Tasks/IPaymoTasksApiService.cs b/src/Tasks/IPaymoTasksApiService.cs new file mode 100644 index 0000000..544cbb1 --- /dev/null +++ b/src/Tasks/IPaymoTasksApiService.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Novaloop.PaymoApi.Tasks.Models; + +namespace Novaloop.PaymoApi.Tasks +{ + public interface IPaymoTasksApiService + { + Task GetTask(int taskId); + Task CreateTask(CreateTaskRequest createTask); + } +} \ No newline at end of file diff --git a/src/Tasks/Models/CreateTaskRequest.cs b/src/Tasks/Models/CreateTaskRequest.cs new file mode 100644 index 0000000..e8d8e50 --- /dev/null +++ b/src/Tasks/Models/CreateTaskRequest.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace Novaloop.PaymoApi.Tasks.Models +{ + public class CreateTaskRequest + { + public string Name { get; set; } + public string Description { get; set; } + public int TasklistId { get; set; } + public List Users { get; set; } + } +} \ No newline at end of file diff --git a/src/Tasks/Models/CreateTaskResponse.cs b/src/Tasks/Models/CreateTaskResponse.cs new file mode 100644 index 0000000..2c5fa15 --- /dev/null +++ b/src/Tasks/Models/CreateTaskResponse.cs @@ -0,0 +1,7 @@ +namespace Novaloop.PaymoApi.Tasks.Models +{ + public class CreateTaskResponse : PaymoTask + { + + } +} \ No newline at end of file diff --git a/src/Tasks/Models/GetTasksResponse.cs b/src/Tasks/Models/GetTasksResponse.cs new file mode 100644 index 0000000..f6c7364 --- /dev/null +++ b/src/Tasks/Models/GetTasksResponse.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Novaloop.PaymoApi.Tasks.Models +{ + public class GetTasksResponse + { + public IEnumerable Tasks { get; set; } + } +} \ No newline at end of file diff --git a/src/Tasks/Models/PaymoTask.cs b/src/Tasks/Models/PaymoTask.cs new file mode 100644 index 0000000..b7a31c5 --- /dev/null +++ b/src/Tasks/Models/PaymoTask.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + +namespace Novaloop.PaymoApi.Tasks.Models +{ + public class PaymoTask + { + public int Id { get; set; } + public string Name { get; set; } + public string Code { get; set; } + public int ProjectId { get; set; } + public int TasklistId { get; set; } + public int UserId { get; set; } + public bool Complete { get; set; } + public bool Billable { get; set; } + public int Seq { get; set; } + public string Description { get; set; } + public object PricePerHour { get; set; } + public object DueDate { get; set; } + public object BudgetHours { get; set; } + public List Users { get; set; } + public DateTime CreatedOn { get; set; } + public DateTime UpdatedOn { get; set; } + } +} \ No newline at end of file diff --git a/src/Tasks/PaymoTasksApiService.cs b/src/Tasks/PaymoTasksApiService.cs new file mode 100644 index 0000000..6509896 --- /dev/null +++ b/src/Tasks/PaymoTasksApiService.cs @@ -0,0 +1,44 @@ +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Novaloop.PaymoApi.Extensions; +using Novaloop.PaymoApi.Shared; +using Novaloop.PaymoApi.Tasks.Models; + +namespace Novaloop.PaymoApi.Tasks +{ + public class PaymoTasksApiService : IPaymoTasksApiService + { + private readonly PaymoApiOptions _options; + private readonly HttpClient _client; + + public PaymoTasksApiService(PaymoApiClient paymoApiClient, IOptions options) + { + _options = options.Value; + _client = paymoApiClient.Client; + } + + /// + /// Get an existing Task + /// + public async Task GetTask(int taskId) + { + _client.SetApiKeyHeader(_options.ApiToken); + var response = await _client.GetAsync($"/tasks/{taskId}"); + await response.ThrowExceptionWithDetailsIfUnsuccessful(); + return await response.Content.ReadAsAsync(); + } + + + /// + /// Creates a new Task + /// + public async Task CreateTask(CreateTaskRequest createTaskRequest) + { + _client.SetApiKeyHeader(_options.ApiToken); + var response = await _client.PostAsJsonAsync("/tasks", createTaskRequest); + await response.ThrowExceptionWithDetailsIfUnsuccessful(); + return await response.Content.ReadAsAsync(); + } + } +} \ No newline at end of file diff --git a/tests/Novaloop.PaymoApi.Tests.csproj b/tests/Novaloop.PaymoApi.Tests.csproj new file mode 100644 index 0000000..c0dc1ac --- /dev/null +++ b/tests/Novaloop.PaymoApi.Tests.csproj @@ -0,0 +1,22 @@ + + + + net5.0 + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/tests/UnitTest1.cs b/tests/UnitTest1.cs new file mode 100644 index 0000000..3ce4835 --- /dev/null +++ b/tests/UnitTest1.cs @@ -0,0 +1,13 @@ +using Xunit; + +namespace Novaloop.PaymoApi.Tests +{ + public class UnitTest1 + { + [Fact] + public void Test1() + { + Assert.True(true); + } + } +} \ No newline at end of file