Skip to content

Commit

Permalink
Merge branch 'release-0.1.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
rroa committed Feb 5, 2024
2 parents d64df34 + f82d28a commit 467abe8
Show file tree
Hide file tree
Showing 35 changed files with 1,075 additions and 84 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ MigrationBackup/
FodyWeavers.xsd

# VS Code files for those working on multiple tools
.vscode/
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
Expand Down
6 changes: 0 additions & 6 deletions .vscode/settings.json

This file was deleted.

9 changes: 9 additions & 0 deletions AlfredApiWrapper/AlfredApiWrapper.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@
<TargetFrameworks>net472;netstandard2.0</TargetFrameworks>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>disable</Nullable>
<PackageId>TagShelf.Alfred.ApiWrapper</PackageId>
<Version>0.1.0</Version>
<Authors>Raúl Roa ([email protected])</Authors>
<Company>Tagshelf</Company>
<Description>A comprehensive .NET library designed to facilitate seamless interactions with the Alfred API.</Description>
<PackageTags>Alfred;API;Wrapper;.NET</PackageTags>
<RepositoryUrl>https://github.com/tagshelfsrl/dotnet-alfred-api-wrapper</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReleaseNotes>Initial release of TagShelf.Alfred.ApiWrapper, supporting comprehensive authentication, domain-specific operations, and cross-platform compatibility.</PackageReleaseNotes>
</PropertyGroup>

<ItemGroup>
Expand Down
22 changes: 2 additions & 20 deletions AlfredApiWrapper/Authentication/OAuthAuthentication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,28 +40,10 @@ public async Task AuthenticateAsync()
new KeyValuePair<string, string>("username", _username),
new KeyValuePair<string, string>("password", _password),
});

HttpResponseMessage response = null;

try
{
response = await _apiClient.PostAsync("/token", content);

// Check if the response is successful (2xx)
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
throw new HttpRequestException($"Authentication failed with status code {response.StatusCode}: {errorContent}");
}

var responseContent = await response.Content.ReadAsStringAsync();
var tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(responseContent);

// Validate the deserialized token response
if (tokenResponse == null || string.IsNullOrWhiteSpace(tokenResponse.AccessToken))
{
throw new InvalidOperationException("Invalid token response received from the server.");
}

var tokenResponse = await _apiClient.PostAsync<TokenResponse>("/token", content);
_apiClient.AddOrUpdateHeader("Authorization", $"Bearer {tokenResponse.AccessToken}");
}
catch (HttpRequestException ex)
Expand Down
30 changes: 28 additions & 2 deletions AlfredApiWrapper/Core/Alfred.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
using System;
using TagShelf.Alfred.ApiWrapper.Domains.DataPoint;
using TagShelf.Alfred.ApiWrapper.Domains.DeferredSession;
using TagShelf.Alfred.ApiWrapper.Domains.File;
using TagShelf.Alfred.ApiWrapper.Domains.Job;
using TagShelf.Alfred.ApiWrapper.Domains.Tagshelf;
using TagShelf.Alfred.ApiWrapper.Enumerations;

namespace TagShelf.Alfred.ApiWrapper.Core
{
/// <summary>
/// Represents the main client for interacting with the Alfred API, supporting various authentication methods.
/// </summary>
public class Alfred
public class Alfred : IAlfred
{
/// <summary>
/// Gets the HttpApiClient used for all HTTP interactions.
Expand All @@ -25,6 +30,7 @@ internal Alfred(EnvironmentType environment)
ApiClient = new HttpApiClient();
_environment = environment;
SetBaseAddress(environment);
InitDomains();
}

/// <summary>
Expand All @@ -47,6 +53,26 @@ private void SetBaseAddress(EnvironmentType environment)
throw new ArgumentException("Unsupported environment type", nameof(environment));
}
ApiClient.SetBaseAddress(baseUrl);
}
}

/// <summary>
/// Initializes the domain-specific properties of the Alfred API wrapper.
/// </summary>
private void InitDomains()
{
Job = new JobDomain(ApiClient);
File = new FileDomain(ApiClient);
Tagshelf = new TagshelfDomain(ApiClient);
DeferredSession = new DeferredSessionDomain(ApiClient);
DataPoint = new DataPointDomain(ApiClient);
}

#region IAlfred implementation
public JobDomain Job { get; private set; }
public FileDomain File { get; private set; }
public TagshelfDomain Tagshelf { get; private set; }
public DeferredSessionDomain DeferredSession { get; private set; }
public DataPointDomain DataPoint { get; private set; }
#endregion
}
}
48 changes: 48 additions & 0 deletions AlfredApiWrapper/Core/ApiException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System;

namespace TagShelf.Alfred.ApiWrapper.Core
{

/// <summary>
/// Represents errors that occur during API request execution.
/// </summary>
public class ApiException : Exception
{
/// <summary>
/// Gets the error code returned by the API.
/// </summary>
public string Error { get; }

/// <summary>
/// Gets the error message returned by the API.
/// </summary>
public string ErrorMessage { get; }

/// <summary>
/// Gets the HTTP status code of the response, if available.
/// </summary>
public System.Net.HttpStatusCode? StatusCode { get; }

/// <summary>
/// Gets the raw content of the error response.
/// </summary>
public string ResponseContent { get; }

/// <summary>
/// Initializes a new instance of the <see cref="ApiException"/> class with specified error details.
/// </summary>
/// <param name="error">The error code returned by the API.</param>
/// <param name="errorMessage">The error message returned by the API.</param>
/// <param name="statusCode">The HTTP status code of the response, if available.</param>
/// <param name="responseContent">The raw content of the error response.</param>
/// <param name="innerException">The inner exception, if any.</param>
public ApiException(string error, string errorMessage, System.Net.HttpStatusCode? statusCode, string responseContent, Exception innerException = null)
: base($"API error: {error} - {errorMessage}", innerException)
{
Error = error;
ErrorMessage = errorMessage;
StatusCode = statusCode;
ResponseContent = responseContent;
}
}
}
16 changes: 16 additions & 0 deletions AlfredApiWrapper/Core/ErrorResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Newtonsoft.Json;

namespace TagShelf.Alfred.ApiWrapper.Core
{
/// <summary>
/// Represents an error response from the API.
/// </summary>
public class ErrorResult
{
[JsonProperty("error")]
public string Error { get; set; }

[JsonProperty("message")]
public string Message { get; set; }
}
}
91 changes: 85 additions & 6 deletions AlfredApiWrapper/Core/HttpApiClient.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace TagShelf.Alfred.ApiWrapper.Core
{
Expand All @@ -10,14 +13,16 @@ namespace TagShelf.Alfred.ApiWrapper.Core
public class HttpApiClient
{
private readonly HttpClient _httpClient;
private readonly string _clientIdentifier;

/// <summary>
/// Initializes a new instance of the HttpApiClient class.
/// </summary>
public HttpApiClient()
{
_httpClient = new HttpClient();
// Initialize your HttpClient instance here
_clientIdentifier = Guid.NewGuid().ToString(); // Unique identifier for this client instance
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd($"dotnet-alfred-api-wrapper/{_clientIdentifier}");
}

/// <summary>
Expand All @@ -27,6 +32,7 @@ public HttpApiClient()
public void SetBaseAddress(string baseAddress)
{
_httpClient.BaseAddress = new Uri(baseAddress);
// _httpClient.DefaultRequestHeaders.Add("Content-Type", "application/json");
}

/// <summary>
Expand All @@ -44,14 +50,87 @@ public void AddOrUpdateHeader(string name, string value)
}

/// <summary>
/// Sends a POST request to the specified Uri as an asynchronous operation.
/// Sends a POST request to the specified Uri as an asynchronous operation and deserializes the JSON response.
/// This method ensures that an empty content request is still sent with a Content-Type of application/json.
/// </summary>
/// <typeparam name="TResult">The type of the deserialized result.</typeparam>
/// <param name="uri">The Uri the request is sent to.</param>
/// <param name="content">The HTTP request content sent to the server.</param>
/// <returns>The HTTP response message.</returns>
public async Task<HttpResponseMessage> PostAsync(string uri, HttpContent content)
/// <param name="content">The HTTP request content sent to the server, or null if no content is being sent.</param>
/// <returns>A task that represents the asynchronous operation, containing the deserialized response.</returns>
public Task<TResult> PostAsync<TResult>(string uri, HttpContent content = null)
{
return await _httpClient.PostAsync(uri, content);
var request = new HttpRequestMessage(HttpMethod.Post, uri)
{
// Use an empty StringContent with application/json type if no content is provided.
Content = content ?? new StringContent(string.Empty, Encoding.UTF8, "application/json")
};
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
return SendAsync<TResult>(request);
}

/// <summary>
/// Helper method to easily create and send a GET request.
/// </summary>
/// <typeparam name="TResult">The expected type of the response object.</typeparam>
/// <param name="uri">The URI the request is sent to.</param>
/// <returns>The deserialized response object.</returns>
public Task<TResult> GetAsync<TResult>(string uri)
{
var request = new HttpRequestMessage(HttpMethod.Get, uri);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
return SendAsync<TResult>(request);
}

/// <summary>
/// Fetches the response for a file download request without deserializing the content, allowing direct access to the byte stream.
/// </summary>
/// <param name="uri">The URI the request is sent to.</param>
/// <returns>A task that represents the asynchronous operation, containing the raw HTTP response.</returns>
public async Task<HttpResponseMessage> GetFileAsync(string uri)
{
var request = new HttpRequestMessage(HttpMethod.Get, uri);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*")); // Accept any content type
var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return response;
}


/// <summary>
/// Sends an HTTP request as an asynchronous operation and deserializes the JSON response to a specified type.
/// Handles non-2xx responses by attempting to deserialize the error response and throwing a detailed exception.
/// </summary>
/// <typeparam name="TResult">The type of the deserialized result.</typeparam>
/// <param name="request">The HTTP request message to send.</param>
/// <returns>A task that represents the asynchronous operation, containing the deserialized response.</returns>
/// <exception cref="ApiException">Thrown when the response indicates an error.</exception>
private async Task<TResult> SendAsync<TResult>(HttpRequestMessage request)
{
HttpResponseMessage response = null;
try
{
response = await _httpClient.SendAsync(request).ConfigureAwait(false);

if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
return JsonConvert.DeserializeObject<TResult>(json);
}
else
{
var errorJson = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var errorResult = JsonConvert.DeserializeObject<ErrorResult>(errorJson);
throw new ApiException(errorResult.Error, errorResult.Message, response.StatusCode, errorJson);
}
}
catch (HttpRequestException ex)
{
throw new ApiException("Network error", ex.Message, null, null, ex);
}
finally
{
response?.Dispose();
}
}
}
}
28 changes: 28 additions & 0 deletions AlfredApiWrapper/Domains/DataPointDomain/DataPointDomain.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using TagShelf.Alfred.ApiWrapper.Core;
using TagShelf.Alfred.ApiWrapper.Domains.DataPoint.Results;

namespace TagShelf.Alfred.ApiWrapper.Domains.DataPoint
{
public class DataPointDomain
{
private readonly HttpApiClient _apiClient;

public DataPointDomain(HttpApiClient apiClient)
{
_apiClient = apiClient;
}

/// <summary>
/// Fetches a list of metadata values for a given file ID.
/// </summary>
/// <param name="fileId">The unique identifier of the file.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of DataPointResult.</returns>
public async Task<List<DataPointResult>> GetValuesAsync(Guid fileId)
{
return await _apiClient.GetAsync<List<DataPointResult>>($"api/values/file/{fileId}").ConfigureAwait(false);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Newtonsoft.Json;
using System;

namespace TagShelf.Alfred.ApiWrapper.Domains.DataPoint.Results
{
public class DataPointResult
{
[JsonProperty("id")]
public Guid Id { get; set; }

[JsonProperty("file_log_id")]
public Guid FileLogId { get; set; }

[JsonProperty("metadata_id")]
public Guid MetadataId { get; set; }

[JsonProperty("metadata_name")]
public string MetadataName { get; set; }

[JsonProperty("value")]
public string Value { get; set; }

[JsonProperty("classification_score")]
public double ClassificationScore { get; set; }
}
}
Loading

0 comments on commit 467abe8

Please sign in to comment.