chore: initial commit

This commit is contained in:
Matthias Langhard
2020-12-07 22:00:24 +01:00
commit 75a687e31e
20 changed files with 507 additions and 0 deletions

117
.gitignore vendored Normal file
View File

@@ -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

18
.gitlab-ci.yml Normal file
View File

@@ -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

48
Novaloop.PaymoApi.sln Normal file
View File

@@ -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

50
README.md Normal file
View File

@@ -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<IActionResult> Get()
{
return Ok(await _paymoTaskService.GetTask(11));
}
}
```

2
pack.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env bash
dotnet pack src -o ../local-nuget-packages

View File

@@ -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; }
}
}

View File

@@ -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<HttpResponseMessage> 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));
}
}
}

View File

@@ -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());
}
}
}
}

View File

@@ -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<PaymoApiOptions> options)
{
services.Configure(options);
var resolvedOptions = (IOptions<PaymoApiOptions>) services.BuildServiceProvider().GetService(typeof(IOptions<PaymoApiOptions>));
services.AddHttpClient<PaymoApiClient>(client => { client.BaseAddress = new Uri(resolvedOptions.Value.BaseUrl); });
services.AddTransient<IPaymoTasksApiService, PaymoTasksApiService>();
return services;
}
}
}

View File

@@ -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; }
}
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageId>Novaloop.PaymoApi</PackageId>
<title>Access your paymo instance for asp.net core</title>
<PackageTags>api;paymo;asp.net core;</PackageTags>
<Version>0.1.0</Version>
<Authors>Matthias Langhard</Authors>
<Company>Novaloop AG</Company>
<PackageProjectUrl>https://gitlab.com/novaloop-oss/novaloop.paymoapi</PackageProjectUrl>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
</ItemGroup>
</Project>

View File

@@ -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; }
}
}

View File

@@ -0,0 +1,11 @@
using System.Threading.Tasks;
using Novaloop.PaymoApi.Tasks.Models;
namespace Novaloop.PaymoApi.Tasks
{
public interface IPaymoTasksApiService
{
Task<GetTasksResponse> GetTask(int taskId);
Task<CreateTaskResponse> CreateTask(CreateTaskRequest createTask);
}
}

View File

@@ -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<int> Users { get; set; }
}
}

View File

@@ -0,0 +1,7 @@
namespace Novaloop.PaymoApi.Tasks.Models
{
public class CreateTaskResponse : PaymoTask
{
}
}

View File

@@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace Novaloop.PaymoApi.Tasks.Models
{
public class GetTasksResponse
{
public IEnumerable<PaymoTask> Tasks { get; set; }
}
}

View File

@@ -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<int> Users { get; set; }
public DateTime CreatedOn { get; set; }
public DateTime UpdatedOn { get; set; }
}
}

View File

@@ -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<PaymoApiOptions> options)
{
_options = options.Value;
_client = paymoApiClient.Client;
}
/// <summary>
/// Get an existing Task
/// </summary>
public async Task<GetTasksResponse> GetTask(int taskId)
{
_client.SetApiKeyHeader(_options.ApiToken);
var response = await _client.GetAsync($"/tasks/{taskId}");
await response.ThrowExceptionWithDetailsIfUnsuccessful();
return await response.Content.ReadAsAsync<GetTasksResponse>();
}
/// <summary>
/// Creates a new Task
/// </summary>
public async Task<CreateTaskResponse> CreateTask(CreateTaskRequest createTaskRequest)
{
_client.SetApiKeyHeader(_options.ApiToken);
var response = await _client.PostAsJsonAsync("/tasks", createTaskRequest);
await response.ThrowExceptionWithDetailsIfUnsuccessful();
return await response.Content.ReadAsAsync<CreateTaskResponse>();
}
}
}

View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="1.3.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>

13
tests/UnitTest1.cs Normal file
View File

@@ -0,0 +1,13 @@
using Xunit;
namespace Novaloop.PaymoApi.Tests
{
public class UnitTest1
{
[Fact]
public void Test1()
{
Assert.True(true);
}
}
}