Implemented Core client

This commit is contained in:
2024-10-23 21:00:23 +02:00
parent 3c8d5137ff
commit 83620cb450
43 changed files with 4218 additions and 2 deletions

View File

@@ -0,0 +1,43 @@
using System;
using System.Net.Http;
using System.Text.RegularExpressions;
namespace AMWD.Net.Api.Cloudflare.Auth
{
/// <summary>
/// Implements the interface to authenticate using an API key and email address.
/// </summary>
public class ApiKeyAuthentication : IAuthentication
{
private static readonly Regex _emailCheckRegex = new(@"^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$", RegexOptions.Compiled);
private readonly string _emailAddress;
private readonly string _apiKey;
/// <summary>
/// Initializes a new instance of the <see cref="ApiKeyAuthentication"/> class.
/// </summary>
/// <param name="emailAddress">The email address.</param>
/// <param name="apiKey">The global API key.</param>
public ApiKeyAuthentication(string emailAddress, string apiKey)
{
if (string.IsNullOrWhiteSpace(emailAddress))
throw new ArgumentNullException(nameof(emailAddress));
if (string.IsNullOrWhiteSpace(apiKey))
throw new ArgumentNullException(nameof(apiKey));
if (!_emailCheckRegex.IsMatch(emailAddress))
throw new ArgumentException("Invalid email address", nameof(emailAddress));
_emailAddress = emailAddress;
_apiKey = apiKey;
}
/// <inheritdoc />
public void AddHeader(HttpClient httpClient)
{
httpClient.DefaultRequestHeaders.Add("X-Auth-Email", _emailAddress);
httpClient.DefaultRequestHeaders.Add("X-Auth-Key", _apiKey);
}
}
}

View File

@@ -0,0 +1,32 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
namespace AMWD.Net.Api.Cloudflare.Auth
{
/// <summary>
/// Implements the interface to authenticate using an API token.
/// </summary>
public class ApiTokenAuthentication : IAuthentication
{
private readonly string _apiToken;
/// <summary>
/// Initializes a new instance of the <see cref="ApiTokenAuthentication"/> class.
/// </summary>
/// <param name="apiToken">The API token.</param>
public ApiTokenAuthentication(string apiToken)
{
if (string.IsNullOrWhiteSpace(apiToken))
throw new ArgumentNullException(nameof(apiToken));
_apiToken = apiToken;
}
/// <inheritdoc />
public void AddHeader(HttpClient httpClient)
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _apiToken);
}
}
}

View File

@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Net;
namespace AMWD.Net.Api.Cloudflare
{
/// <summary>
/// Options for the Cloudflare API.
/// </summary>
public class ClientOptions
{
/// <summary>
/// Gets or sets the default base url for the API.
/// </summary>
public virtual string BaseUrl { get; set; } = "https://api.cloudflare.com/client/v4/";
/// <summary>
/// Gets or sets the default timeout for the API.
/// </summary>
public virtual TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(60);
/// <summary>
/// Gets or sets the maximum number of retries for the API.
/// </summary>
/// <remarks>
/// The API may respond with an 5xx error and a X-Should-Retry header indicating that the request should be retried.
/// </remarks>
public virtual int MaxRetries { get; set; } = 2;
/// <summary>
/// Gets or sets additional default headers to every request.
/// </summary>
public virtual IDictionary<string, string> DefaultHeaders { get; set; } = new Dictionary<string, string>();
/// <summary>
/// Gets or sets additional default query parameters to every request.
/// </summary>
public virtual IDictionary<string, string> DefaultQueryParams { get; set; } = new Dictionary<string, string>();
/// <summary>
/// Gets or sets a value indicating whether to allow redirects.
/// </summary>
public virtual bool AllowRedirects { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to use a proxy.
/// </summary>
public virtual bool UseProxy { get; set; }
/// <summary>
/// Gets or sets the proxy information.
/// </summary>
public virtual IWebProxy Proxy { get; set; }
}
}

View File

@@ -0,0 +1,62 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<NrtRevisionFormat>{semvertag:main}{!:-dev}</NrtRevisionFormat>
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
<CopyRefAssembliesToPublishDirectory>false</CopyRefAssembliesToPublishDirectory>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/AM-WD/cloudflare-api.git</RepositoryUrl>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageIcon>package-icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageTags>cloudflare api</PackageTags>
<PackageProjectUrl>https://developers.cloudflare.com/api</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<EmbedUntrackedSources>false</EmbedUntrackedSources>
<PackageId>AMWD.Net.API.Cloudflare</PackageId>
<AssemblyName>amwd-cloudflare-core</AssemblyName>
<RootNamespace>AMWD.Net.Api.Cloudflare</RootNamespace>
<Product>Cloudflare API - Core</Product>
<Description>Core features of the Cloudflare API</Description>
</PropertyGroup>
<PropertyGroup Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('$(CI_COMMIT_TAG)', '^v[0-9.]+'))">
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
<PropertyGroup Condition="'$(GITLAB_CI)' == 'true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<ItemGroup Condition="'$(GITLAB_CI)' == 'true'">
<SourceLinkGitLabHost Include="$(CI_SERVER_HOST)" Version="$(CI_SERVER_VERSION)" />
<PackageReference Include="Microsoft.SourceLink.GitLab" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<None Include="$(SolutionDir)/package-icon.png" Pack="true" PackagePath="/" />
<None Include="README.md" Pack="true" PackagePath="/" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AMWD.NetRevisionTask" Version="1.2.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,324 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Security.Authentication;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AMWD.Net.Api.Cloudflare.Auth;
namespace AMWD.Net.Api.Cloudflare
{
/// <summary>
/// Implements the Core of the Cloudflare API client.
/// </summary>
public partial class CloudflareClient : ICloudflareClient, IDisposable
{
private static readonly JsonSerializerSettings _jsonSerializerSettings = new JsonSerializerSettings
{
Culture = CultureInfo.InvariantCulture,
Formatting = Formatting.None,
NullValueHandling = NullValueHandling.Ignore,
};
private readonly ClientOptions _clientOptions;
private readonly HttpClient _httpClient;
private bool _isDisposed = false;
/// <summary>
/// Initializes a new instance of the <see cref="CloudflareClient"/> class.
/// </summary>
/// <param name="emailAddress">The email address of the Cloudflare account.</param>
/// <param name="apiKey">The API key of the Cloudflare account.</param>
/// <param name="clientOptions">The client options (optional).</param>
public CloudflareClient(string emailAddress, string apiKey, ClientOptions clientOptions = null)
: this(new ApiKeyAuthentication(emailAddress, apiKey), clientOptions)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="CloudflareClient"/> class.
/// </summary>
/// <param name="apiToken">The API token.</param>
/// <param name="clientOptions">The client options (optional).</param>
public CloudflareClient(string apiToken, ClientOptions clientOptions = null)
: this(new ApiTokenAuthentication(apiToken), clientOptions)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="CloudflareClient"/> class.
/// </summary>
/// <param name="authentication">The authentication information.</param>
/// <param name="clientOptions">The client options (optional).</param>
public CloudflareClient(IAuthentication authentication, ClientOptions clientOptions = null)
{
if (authentication == null)
throw new ArgumentNullException(nameof(authentication));
_clientOptions = clientOptions ?? new ClientOptions();
ValidateClientOptions();
_httpClient = CreateHttpClient();
authentication.AddHeader(_httpClient);
}
/// <summary>
/// Disposes of the resources used by the <see cref="CloudflareClient"/> object.
/// </summary>
public void Dispose()
{
if (_isDisposed)
return;
_isDisposed = true;
_httpClient.Dispose();
GC.SuppressFinalize(this);
}
/// <inheritdoc/>
public async Task<CloudflareResponse<TResponse>> GetAsync<TResponse>(string requestPath, IQueryParameterFilter queryFilter = null, CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
ValidateRequestPath(requestPath);
string requestUrl = BuildRequestUrl(requestPath, queryFilter);
var response = await _httpClient.GetAsync(requestUrl, cancellationToken).ConfigureAwait(false);
return await GetCloudflareResponse<TResponse>(response, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task<CloudflareResponse<TResponse>> PostAsync<TResponse, TRequest>(string requestPath, TRequest request, CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
ValidateRequestPath(requestPath);
string requestUrl = BuildRequestUrl(requestPath);
HttpContent httpRequestContent;
if (request is HttpContent httpContent)
{
httpRequestContent = httpContent;
}
else
{
string json = JsonConvert.SerializeObject(request, _jsonSerializerSettings);
httpRequestContent = new StringContent(json, Encoding.UTF8, "application/json");
}
var response = await _httpClient.PostAsync(requestUrl, httpRequestContent, cancellationToken).ConfigureAwait(false);
return await GetCloudflareResponse<TResponse>(response, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task<CloudflareResponse<TResponse>> PutAsync<TResponse, TRequest>(string requestPath, TRequest request, CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
ValidateRequestPath(requestPath);
string requestUrl = BuildRequestUrl(requestPath);
HttpContent httpRequestContent;
if (request is HttpContent httpContent)
{
httpRequestContent = httpContent;
}
else
{
string json = JsonConvert.SerializeObject(request, _jsonSerializerSettings);
httpRequestContent = new StringContent(json, Encoding.UTF8, "application/json");
}
var response = await _httpClient.PutAsync(requestUrl, httpRequestContent, cancellationToken).ConfigureAwait(false);
return await GetCloudflareResponse<TResponse>(response, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task<CloudflareResponse<TResponse>> DeleteAsync<TResponse>(string requestPath, CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
ValidateRequestPath(requestPath);
string requestUrl = BuildRequestUrl(requestPath);
var response = await _httpClient.DeleteAsync(requestUrl, cancellationToken).ConfigureAwait(false);
return await GetCloudflareResponse<TResponse>(response, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task<CloudflareResponse<TResponse>> PatchAsync<TResponse, TRequest>(string requestPath, TRequest request, CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
ValidateRequestPath(requestPath);
string requestUrl = BuildRequestUrl(requestPath);
HttpContent httpRequestContent;
if (request is HttpContent httpContent)
{
httpRequestContent = httpContent;
}
else
{
string json = JsonConvert.SerializeObject(request, _jsonSerializerSettings);
httpRequestContent = new StringContent(json, Encoding.UTF8, "application/json");
}
#if NET6_0_OR_GREATER
var response = await _httpClient.PatchAsync(requestUrl, httpRequestContent, cancellationToken).ConfigureAwait(false);
#else
var httpRequestMessage = new HttpRequestMessage
{
Version = HttpVersion.Version11,
Method = new HttpMethod("PATCH"),
RequestUri = new Uri(requestUrl),
Content = httpRequestContent,
};
var response = await _httpClient.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false);
#endif
return await GetCloudflareResponse<TResponse>(response, cancellationToken).ConfigureAwait(false);
}
private void ThrowIfDisposed()
{
if (_isDisposed)
throw new ObjectDisposedException(GetType().FullName);
}
private void ValidateClientOptions()
{
if (string.IsNullOrWhiteSpace(_clientOptions.BaseUrl))
throw new ArgumentNullException(nameof(_clientOptions.BaseUrl));
if (_clientOptions.Timeout <= TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(_clientOptions.Timeout), "Timeout must be positive.");
if (_clientOptions.MaxRetries < 0 || 10 < _clientOptions.MaxRetries)
throw new ArgumentOutOfRangeException(nameof(_clientOptions.MaxRetries), "MaxRetries should be between 0 and 10.");
if (_clientOptions.UseProxy && _clientOptions.Proxy == null)
throw new ArgumentNullException(nameof(_clientOptions.Proxy));
}
private void ValidateRequestPath(string requestPath)
{
if (string.IsNullOrWhiteSpace(requestPath))
throw new ArgumentNullException(nameof(requestPath));
if (requestPath.Contains("?"))
throw new ArgumentException("Query parameters are not allowed", nameof(requestPath));
}
private HttpClient CreateHttpClient()
{
string version = typeof(CloudflareClient).Assembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
.InformationalVersion;
HttpMessageHandler handler;
try
{
handler = new HttpClientHandler
{
AllowAutoRedirect = _clientOptions.AllowRedirects,
UseProxy = _clientOptions.UseProxy,
Proxy = _clientOptions.Proxy,
};
}
catch (PlatformNotSupportedException)
{
handler = new HttpClientHandler
{
AllowAutoRedirect = _clientOptions.AllowRedirects,
};
}
var client = new HttpClient(handler, true)
{
BaseAddress = new Uri(_clientOptions.BaseUrl),
Timeout = _clientOptions.Timeout,
};
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("AMWD.CloudflareClient", version));
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
if (_clientOptions.DefaultHeaders.Count > 0)
{
foreach (var headerKvp in _clientOptions.DefaultHeaders)
client.DefaultRequestHeaders.Add(headerKvp.Key, headerKvp.Value);
}
return client;
}
private async Task<CloudflareResponse<TRes>> GetCloudflareResponse<TRes>(HttpResponseMessage response, CancellationToken cancellationToken)
{
#if NET6_0_OR_GREATER
string content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
#else
string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
#endif
switch (response.StatusCode)
{
case HttpStatusCode.Forbidden:
case HttpStatusCode.Unauthorized:
var errorResponse = JsonConvert.DeserializeObject<CloudflareResponse<object>>(content);
throw new AuthenticationException(string.Join(Environment.NewLine, errorResponse.Errors.Select(e => $"{e.Code}: {e.Message}")));
default:
try
{
return JsonConvert.DeserializeObject<CloudflareResponse<TRes>>(content);
}
catch
{
if (typeof(TRes) == typeof(string))
{
object cObj = content.Replace("\\n", Environment.NewLine);
return new CloudflareResponse<TRes>
{
Success = true,
ResultInfo = new ResultInfo(),
Result = (TRes)cObj,
};
}
throw;
}
}
}
private string BuildRequestUrl(string requestPath, IQueryParameterFilter queryFilter = null)
{
var dict = new Dictionary<string, string>();
if (_clientOptions.DefaultQueryParams.Count > 0)
{
foreach (var paramKvp in _clientOptions.DefaultQueryParams)
dict[paramKvp.Key] = paramKvp.Value;
}
var queryParams = queryFilter?.GetQueryParameters();
if (queryParams?.Count > 0)
{
foreach (var kvp in queryParams)
dict[kvp.Key] = kvp.Value;
}
if (dict.Count == 0)
return requestPath;
string[] param = dict.Select(kvp => $"{kvp.Key}={WebUtility.UrlEncode(kvp.Value)}").ToArray();
string query = string.Join("&", param);
return $"{requestPath}?{query}";
}
}
}

View File

@@ -0,0 +1,24 @@
using System.Runtime.Serialization;
using Newtonsoft.Json.Converters;
namespace AMWD.Net.Api.Cloudflare
{
/// <summary>
/// Whether to match all search requirements or at least one (any).
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public enum FilterMatchType
{
/// <summary>
/// Match all search requirements.
/// </summary>
[EnumMember(Value = "all")]
All = 1,
/// <summary>
/// Match at least one search requirement.
/// </summary>
[EnumMember(Value = "any")]
Any = 2,
}
}

View File

@@ -0,0 +1,24 @@
using System.Runtime.Serialization;
using Newtonsoft.Json.Converters;
namespace AMWD.Net.Api.Cloudflare
{
/// <summary>
/// The direction to order the entity.
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public enum OrderDirection
{
/// <summary>
/// Order in ascending order.
/// </summary>
[EnumMember(Value = "asc")]
Asc = 1,
/// <summary>
/// Order in descending order.
/// </summary>
[EnumMember(Value = "desc")]
Desc = 2
}
}

View File

@@ -0,0 +1,61 @@
using System;
using System.Runtime.Serialization;
namespace AMWD.Net.Api.Cloudflare
{
/// <summary>
/// Represents errors that occur during Cloudflare API calls.
/// </summary>
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public class CloudflareException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="CloudflareException"/> class.
/// </summary>
public CloudflareException()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CloudflareException"/> class with a specified error
/// message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public CloudflareException(string message)
: base(message)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CloudflareException"/> class with a specified error
/// message and a reference to the inner exception that is the cause of this exception.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">
/// The exception that is the cause of the current exception, or a <see langword="null"/> reference
/// if no inner exception is specified.
/// </param>
public CloudflareException(string message, Exception innerException)
: base(message, innerException)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CloudflareException"/> class with serialized data.
/// </summary>
/// <param name="info">
/// The <see cref="SerializationInfo"/> that holds the serialized
/// object data about the exception being thrown.
/// </param>
/// <param name="context">
/// The <see cref="StreamingContext"/> that contains contextual information
/// about the source or destination.
/// </param>
/// <exception cref="ArgumentNullException">The info parameter is null.</exception>
/// <exception cref="SerializationException">The class name is <see langword="null"/> or <see cref="Exception.HResult"/> is zero (0).</exception>
protected CloudflareException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Linq;
using System.Runtime.Serialization;
namespace AMWD.Net.Api.Cloudflare
{
/// <summary>
/// Extension methods for <see cref="Enum"/>s.
/// </summary>
public static class EnumExtensions
{
/// <summary>
/// Gets the <see cref="EnumMemberAttribute.Value"/> of the <see cref="Enum"/> when available, otherwise the <see cref="Enum.ToString()"/>.
/// </summary>
/// <param name="value">The enum value.</param>
public static string GetEnumMemberValue(this Enum value)
{
var fieldInfo = value.GetType().GetField(value.ToString());
if (fieldInfo == null)
return value.ToString();
var enumMember = fieldInfo
.GetCustomAttributes(typeof(EnumMemberAttribute), inherit: false)
.Cast<EnumMemberAttribute>()
.FirstOrDefault();
if (enumMember == null)
return value.ToString();
return enumMember.Value;
}
}
}

View File

@@ -0,0 +1,16 @@
using System.Net.Http;
namespace AMWD.Net.Api.Cloudflare.Auth
{
/// <summary>
/// Defines the interface to add authentication information.
/// </summary>
public interface IAuthentication
{
/// <summary>
/// Adds authentication headers to the given <see cref="HttpClient"/>.
/// </summary>
/// <param name="httpClient">The <see cref="HttpClient"/> to add the headers to.</param>
void AddHeader(HttpClient httpClient);
}
}

View File

@@ -0,0 +1,75 @@
using System.Threading;
using System.Threading.Tasks;
namespace AMWD.Net.Api.Cloudflare
{
/// <summary>
/// Represents a client for the Cloudflare API.
/// </summary>
public interface ICloudflareClient
{
/// <summary>
/// Makes a GET request to the Cloudflare API.
/// </summary>
/// <remarks>
/// The GET method requests a representation of the specified resource.
/// Requests using GET should only retrieve data and should not contain a request content.
/// </remarks>
/// <typeparam name="TResponse">The response type.</typeparam>
/// <param name="requestPath">The request path (extending the base URL).</param>
/// <param name="queryFilter">The query parameters.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
Task<CloudflareResponse<TResponse>> GetAsync<TResponse>(string requestPath, IQueryParameterFilter queryFilter = null, CancellationToken cancellationToken = default);
/// <summary>
/// Makes a POST request to the Cloudflare API.
/// </summary>
/// <remarks>
/// The POST method submits an entity to the specified resource, often causing a change in state or side effects on the server.
/// </remarks>
/// <typeparam name="TResponse">The response type.</typeparam>
/// <typeparam name="TRequest">The request type.</typeparam>
/// <param name="requestPath">The request path (extending the base URL).</param>
/// <param name="request">The request content.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
Task<CloudflareResponse<TResponse>> PostAsync<TResponse, TRequest>(string requestPath, TRequest request, CancellationToken cancellationToken = default);
/// <summary>
/// Makes a PUT request to the Cloudflare API.
/// </summary>
/// <remarks>
/// The PUT method replaces all current representations of the target resource with the request content.
/// </remarks>
/// <typeparam name="TResponse">The response type.</typeparam>
/// <typeparam name="TRequest">The request type.</typeparam>
/// <param name="requestPath">The request path (extending the base URL).</param>
/// <param name="request">The request content.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
Task<CloudflareResponse<TResponse>> PutAsync<TResponse, TRequest>(string requestPath, TRequest request, CancellationToken cancellationToken = default);
/// <summary>
/// Makes a DELETE request to the Cloudflare API.
/// </summary>
/// <remarks>
/// The DELETE method deletes the specified resource.
/// </remarks>
/// <typeparam name="TResponse">The response type.</typeparam>
/// <param name="requestPath">The request path (extending the base URL).</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
/// <returns></returns>
Task<CloudflareResponse<TResponse>> DeleteAsync<TResponse>(string requestPath, CancellationToken cancellationToken = default);
/// <summary>
/// Makes a PATCH request to the Cloudflare API.
/// </summary>
/// <remarks>
/// The PATCH method applies partial modifications to a resource.
/// </remarks>
/// <typeparam name="TResponse">The response type.</typeparam>
/// <typeparam name="TRequest">The request type.</typeparam>
/// <param name="requestPath">The request path (extending the base URL).</param>
/// <param name="request">The request content.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
Task<CloudflareResponse<TResponse>> PatchAsync<TResponse, TRequest>(string requestPath, TRequest request, CancellationToken cancellationToken = default);
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
namespace AMWD.Net.Api.Cloudflare
{
/// <summary>
/// Represents filter options defined via query parameters.
/// </summary>
public interface IQueryParameterFilter
{
/// <summary>
/// Gets the query parameters.
/// </summary>
IDictionary<string, string> GetQueryParameters();
}
}

View File

@@ -0,0 +1,21 @@
namespace AMWD.Net.Api.Cloudflare
{
/// <summary>
/// Base implementation of an account.
/// </summary>
public class AccountBase
{
/// <summary>
/// Identifier
/// </summary>
// <= 32 characters
[JsonProperty("id")]
public string Id { get; set; }
/// <summary>
/// The name of the account.
/// </summary>
[JsonProperty("name")]
public string Name { get; set; }
}
}

View File

@@ -0,0 +1,27 @@
namespace AMWD.Net.Api.Cloudflare
{
/// <summary>
/// Base implementation of an owner.
/// </summary>
public class OwnerBase
{
/// <summary>
/// Identifier.
/// </summary>
// <= 32 characters
[JsonProperty("id")]
public string Id { get; set; }
/// <summary>
/// Name of the owner.
/// </summary>
[JsonProperty("name")]
public string Name { get; set; }
/// <summary>
/// The type of owner.
/// </summary>
[JsonProperty("type")]
public string Type { get; set; }
}
}

21
Cloudflare/README.md Normal file
View File

@@ -0,0 +1,21 @@
# Cloudflare API for .NET | Core
This is the core package for all extensions of the Cloudflare API implemented by [AM.WD].
## Contents
- The `(I)CloudflareClient` with base calls for `GET`, `POST`, `PUT`, `PATCH` and `DELETE` requests.
- Base classes to receive responses.
- `CloudflareException` to specify some errors.
- `IAuthentication` implementations to allow API-Token and API-Key (legacy) authentication.
Any specific request will be defined via extension packages.
---
Published under MIT License (see [choose a license])
[AM.WD]: https://www.nuget.org/packages?q=AMWD.&sortby=created-desc
[choose a license]: https://choosealicense.com/licenses/mit/

View File

@@ -0,0 +1,47 @@
using System.Collections.Generic;
namespace AMWD.Net.Api.Cloudflare
{
/// <summary>
/// The base Cloudflare response.
/// </summary>
public class CloudflareResponse
{
/// <summary>
/// Information about the result of the request.
/// </summary>
[JsonProperty("result_info")]
public ResultInfo ResultInfo { get; set; }
/// <summary>
/// Whether the API call was successful.
/// </summary>
[JsonProperty("success")]
public bool Success { get; set; }
/// <summary>
/// Errors returned by the API call.
/// </summary>
[JsonProperty("errors")]
public IReadOnlyList<ResponseInfo> Errors { get; set; } = [];
/// <summary>
/// Messages returned by the API call.
/// </summary>
[JsonProperty("messages")]
public IReadOnlyList<ResponseInfo> Messages { get; set; } = [];
}
/// <summary>
/// The base Cloudflare response with a result.
/// </summary>
/// <typeparam name="T"></typeparam>
public class CloudflareResponse<T> : CloudflareResponse
{
/// <summary>
/// The result of the API call.
/// </summary>
[JsonProperty("result")]
public T Result { get; set; }
}
}

View File

@@ -0,0 +1,20 @@
namespace AMWD.Net.Api.Cloudflare
{
/// <summary>
/// A Cloudflare response information.
/// </summary>
public class ResponseInfo
{
/// <summary>
/// The message code.
/// </summary>
[JsonProperty("code")]
public int Code { get; set; }
/// <summary>
/// The message.
/// </summary>
[JsonProperty("message")]
public string Message { get; set; }
}
}

View File

@@ -0,0 +1,32 @@
namespace AMWD.Net.Api.Cloudflare
{
/// <summary>
/// Cloudflare Result Information.
/// </summary>
public class ResultInfo
{
/// <summary>
/// Total number of results for the requested service.
/// </summary>
[JsonProperty("count")]
public int Count { get; set; }
/// <summary>
/// Current page within paginated list of results.
/// </summary>
[JsonProperty("page")]
public int Page { get; set; }
/// <summary>
/// Number of results per page of results.
/// </summary>
[JsonProperty("per_page")]
public int PerPage { get; set; }
/// <summary>
/// Total results available without any search parameters.
/// </summary>
[JsonProperty("total_count")]
public int TotalCount { get; set; }
}
}