Skip to content

Commit

Permalink
feat: Add methods for tasks and document creation endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
VMelnalksnis committed Jan 18, 2024
1 parent ea59b38 commit d6a90cc
Show file tree
Hide file tree
Showing 24 changed files with 489 additions and 32 deletions.
11 changes: 11 additions & 0 deletions Directory.Build.props
Expand Up @@ -37,6 +37,17 @@
</PackageReference>
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="IsExternalInit">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Nullable">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<AdditionalFiles Include="$(MSBuildThisFileDirectory)stylecop.json">
<Link>stylecop.json</Link>
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Expand Up @@ -31,7 +31,7 @@
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556"/>
<PackageVersion Include="Testcontainers.Redis" Version="3.4.0"/>
<PackageVersion Include="VMelnalksnis.Testcontainers.Paperless" Version="0.2.0"/>
<PackageVersion Include="xunit" Version="2.6.5"/>
<PackageVersion Include="xunit" Version="2.6.6"/>
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5"/>
</ItemGroup>
</Project>
Expand Up @@ -14,6 +14,7 @@
using VMelnalksnis.PaperlessDotNet.Correspondents;
using VMelnalksnis.PaperlessDotNet.Documents;
using VMelnalksnis.PaperlessDotNet.Serialization;
using VMelnalksnis.PaperlessDotNet.Tasks;

#if NET6_0_OR_GREATER
using System.Net.Mime;
Expand Down Expand Up @@ -53,19 +54,26 @@ static ServiceCollectionExtensions()

return serviceCollection
.AddSingleton<PaperlessJsonSerializerOptions>()
.AddTransient<IPaperlessClient, PaperlessClient>()
.AddTransient<ICorrespondentClient, CorrespondentClient>(provider =>
.AddScoped<IPaperlessClient, PaperlessClient>()
.AddScoped<ITaskClient, TaskClient>(provider =>
{
var httpClient = provider.GetRequiredService<IHttpClientFactory>().CreateClient(PaperlessOptions.Name);
var options = provider.GetRequiredService<PaperlessJsonSerializerOptions>();
return new(httpClient, options);
})
.AddTransient<IDocumentClient, DocumentClient>(provider =>
.AddScoped<ICorrespondentClient, CorrespondentClient>(provider =>
{
var httpClient = provider.GetRequiredService<IHttpClientFactory>().CreateClient(PaperlessOptions.Name);
var options = provider.GetRequiredService<PaperlessJsonSerializerOptions>();
return new(httpClient, options);
})
.AddScoped<IDocumentClient, DocumentClient>(provider =>
{
var httpClient = provider.GetRequiredService<IHttpClientFactory>().CreateClient(PaperlessOptions.Name);
var options = provider.GetRequiredService<PaperlessJsonSerializerOptions>();
var taskClient = provider.GetRequiredService<ITaskClient>();
return new(httpClient, options, taskClient);
})
.AddHttpClient(PaperlessOptions.Name, (provider, client) =>
{
var options = provider.GetRequiredService<IOptionsMonitor<PaperlessOptions>>().CurrentValue;
Expand Down
Expand Up @@ -26,15 +26,4 @@
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="IsExternalInit">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Nullable">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

</Project>
Expand Up @@ -66,25 +66,14 @@ public async Task<Correspondent> Create(CorrespondentCreation correspondent)
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("/api/correspondents/", content).ConfigureAwait(false);

await EnsureSuccessStatusCode(response).ConfigureAwait(false);
await response.EnsureSuccessStatusCodeAsync().ConfigureAwait(false);
return (await response.Content.ReadFromJsonAsync(_context.Correspondent).ConfigureAwait(false))!;
}

/// <inheritdoc />
public async Task Delete(int id)
{
var response = await _httpClient.DeleteAsync($"/api/correspondents/{id}/").ConfigureAwait(false);
await EnsureSuccessStatusCode(response).ConfigureAwait(false);
}

private static async Task EnsureSuccessStatusCode(HttpResponseMessage response)
{
if (response.IsSuccessStatusCode)
{
return;
}

var message = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new HttpRequestException(message);
await response.EnsureSuccessStatusCodeAsync().ConfigureAwait(false);
}
}
4 changes: 2 additions & 2 deletions source/VMelnalksnis.PaperlessDotNet/Documents/Document.cs
Expand Up @@ -18,9 +18,9 @@ public sealed class Document

/// <summary>Gets or sets the archive serial number.</summary>
[JsonPropertyName("archive_serial_number")]
public int? ArchiveSerialNumber { get; set; }
public uint? ArchiveSerialNumber { get; set; }

/// <summary>Gets or sets the correspondent id.</summary>
/// <summary>Gets or sets the <see cref="Correspondents.Correspondent"/> id.</summary>
[JsonPropertyName("correspondent")]
public int? CorrespondentId { get; set; }

Expand Down
87 changes: 86 additions & 1 deletion source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs
Expand Up @@ -2,29 +2,39 @@
// Licensed under the Apache License 2.0.
// See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

using NodaTime.Text;

using VMelnalksnis.PaperlessDotNet.Serialization;
using VMelnalksnis.PaperlessDotNet.Tasks;

namespace VMelnalksnis.PaperlessDotNet.Documents;

/// <inheritdoc />
public sealed class DocumentClient : IDocumentClient
{
private static readonly Version _documentIdVersion = new(1, 9, 2);

private readonly HttpClient _httpClient;
private readonly PaperlessJsonSerializerContext _context;
private readonly ITaskClient _taskClient;

/// <summary>Initializes a new instance of the <see cref="DocumentClient"/> class.</summary>
/// <param name="httpClient">Http client configured for making requests to the Paperless API.</param>
/// <param name="serializerOptions">Paperless specific instance of <see cref="JsonSerializerOptions"/>.</param>
public DocumentClient(HttpClient httpClient, PaperlessJsonSerializerOptions serializerOptions)
/// <param name="taskClient">Paperless task API client.</param>
public DocumentClient(HttpClient httpClient, PaperlessJsonSerializerOptions serializerOptions, ITaskClient taskClient)
{
_httpClient = httpClient;
_taskClient = taskClient;
_context = serializerOptions.Context;
}

Expand Down Expand Up @@ -54,4 +64,79 @@ public IAsyncEnumerable<Document> GetAll(int pageSize, CancellationToken cancell
_context.Document,
cancellationToken);
}

/// <inheritdoc />
public async Task<DocumentCreationResult> Create(DocumentCreation document)
{
var content = new MultipartFormDataContent();
content.Add(new StreamContent(document.Document), "document", document.FileName);

if (document.Title is { } title)
{
content.Add(new StringContent(title), "title");
}

if (document.Created is { } created)
{
content.Add(new StringContent(InstantPattern.General.Format(created)), "created");
}

if (document.CorrespondentId is { } correspondent)
{
content.Add(new StringContent(correspondent.ToString()), "correspondent");
}

if (document.DocumentTypeId is { } documentType)
{
content.Add(new StringContent(documentType.ToString()), "document_type");

Check warning on line 91 in source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs

View check run for this annotation

Codecov / codecov/patch

source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs#L91

Added line #L91 was not covered by tests
}

if (document.StoragePathId is { } storagePath)
{
content.Add(new StringContent(storagePath.ToString()), "storage_path");

Check warning on line 96 in source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs

View check run for this annotation

Codecov / codecov/patch

source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs#L96

Added line #L96 was not covered by tests
}

foreach (var tag in document.TagIds)
{
content.Add(new StringContent(tag.ToString()), "tags");

Check warning on line 101 in source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs

View check run for this annotation

Codecov / codecov/patch

source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs#L101

Added line #L101 was not covered by tests
}

if (document.ArchiveSerialNumber is { } archiveSerialNumber)
{
content.Add(new StringContent(archiveSerialNumber.ToString()), "archive_serial_number");
}

var response = await _httpClient.PostAsync("/api/documents/post_document/", content).ConfigureAwait(false);
await response.EnsureSuccessStatusCodeAsync().ConfigureAwait(false);

// Until v1.9.2 paperless did not return the document import task id,
// so it is not possible to get the document id
var versionHeader = response.Headers.GetValues("x-version").SingleOrDefault();
if (versionHeader is null || !Version.TryParse(versionHeader, out var version) || version <= _documentIdVersion)
{
return new ImportStarted();

Check warning on line 117 in source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs

View check run for this annotation

Codecov / codecov/patch

source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs#L117

Added line #L117 was not covered by tests
}

var id = await response.Content.ReadFromJsonAsync(_context.Guid).ConfigureAwait(false);
var task = await _taskClient.Get(id).ConfigureAwait(false);

while (task is not null && !task.Status.IsCompleted)
{
await Task.Delay(100).ConfigureAwait(false);
task = await _taskClient.Get(id).ConfigureAwait(false);
}

return task switch
{
null => new ImportFailed($"Could not find the import task by the given id {id}"),

Check warning on line 131 in source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs

View check run for this annotation

Codecov / codecov/patch

source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs#L131

Added line #L131 was not covered by tests

_ when task.RelatedDocument is { } documentId => new DocumentCreated(documentId),

_ when task.Status == PaperlessTaskStatus.Success => new ImportFailed(
$"Task status is {PaperlessTaskStatus.Success.Name}, but document id was not given"),

Check warning on line 136 in source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs

View check run for this annotation

Codecov / codecov/patch

source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs#L136

Added line #L136 was not covered by tests
_ when task.Status == PaperlessTaskStatus.Failure => new ImportFailed(task.Result),

_ => throw new ArgumentOutOfRangeException(nameof(task.Status), task.Status, "Unexpected task result"),

Check warning on line 139 in source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs

View check run for this annotation

Codecov / codecov/patch

source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs#L139

Added line #L139 was not covered by tests
};
}
}
54 changes: 54 additions & 0 deletions source/VMelnalksnis.PaperlessDotNet/Documents/DocumentCreation.cs
@@ -0,0 +1,54 @@
// Copyright 2022 Valters Melnalksnis
// Licensed under the Apache License 2.0.
// See LICENSE file in the project root for full license information.

using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;

using NodaTime;

namespace VMelnalksnis.PaperlessDotNet.Documents;

/// <summary>Information needed to create a new <see cref="Document"/>.</summary>
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global", Justification = "Required endpoints for testing not implemented")]
public sealed class DocumentCreation
{
/// <summary>Initializes a new instance of the <see cref="DocumentCreation"/> class.</summary>
/// <param name="document">The document content.</param>
/// <param name="fileName">The name of the file.</param>
public DocumentCreation(Stream document, string fileName)
{
Document = document;
FileName = fileName;

TagIds = Array.Empty<int>();
}

/// <summary>Gets the content of the document.</summary>
public Stream Document { get; }

/// <inheritdoc cref="Document.OriginalFileName"/>
public string FileName { get; }

/// <inheritdoc cref="Document.Created"/>
public Instant? Created { get; init; }

/// <inheritdoc cref="Document.Title"/>
public string? Title { get; init; }

/// <inheritdoc cref="Document.CorrespondentId"/>
public int? CorrespondentId { get; init; }

/// <inheritdoc cref="Document.DocumentTypeId"/>
public int? DocumentTypeId { get; init; }

/// <summary>Gets the id of the storage path.</summary>
public int? StoragePathId { get; init; }

/// <inheritdoc cref="Document.TagIds"/>
public int[] TagIds { get; init; }

/// <inheritdoc cref="Document.ArchivedFileName"/>
public uint? ArchiveSerialNumber { get; init; }
}
@@ -0,0 +1,32 @@
// Copyright 2022 Valters Melnalksnis
// Licensed under the Apache License 2.0.
// See LICENSE file in the project root for full license information.

namespace VMelnalksnis.PaperlessDotNet.Documents;
#pragma warning disable SA1402

/// <summary>Base class for possible results of creating a new document.</summary>
/// <seealso cref="ImportStarted"/>
/// <seealso cref="DocumentCreated"/>
/// <seealso cref="ImportFailed"/>
public abstract class DocumentCreationResult;

/// <summary>Document was successfully created.</summary>
/// <param name="id">The id of the created document.</param>
public sealed class DocumentCreated(int id) : DocumentCreationResult
{
/// <summary>Gets the id of the created document.</summary>
public int Id { get; } = id;
}

/// <summary>Document was successfully submitted and import was started.</summary>
/// <remarks>This is only returned for version below or equal to 1.9.2.</remarks>
public sealed class ImportStarted : DocumentCreationResult;

/// <summary>Document import process failed.</summary>
/// <param name="result">The result message returned by paperless.</param>
public sealed class ImportFailed(string? result) : DocumentCreationResult

Check warning on line 28 in source/VMelnalksnis.PaperlessDotNet/Documents/DocumentCreationResult.cs

View check run for this annotation

Codecov / codecov/patch

source/VMelnalksnis.PaperlessDotNet/Documents/DocumentCreationResult.cs#L28

Added line #L28 was not covered by tests
{
/// <summary>Gets the result message returned by paperless.</summary>
public string? Result { get; } = result;

Check warning on line 31 in source/VMelnalksnis.PaperlessDotNet/Documents/DocumentCreationResult.cs

View check run for this annotation

Codecov / codecov/patch

source/VMelnalksnis.PaperlessDotNet/Documents/DocumentCreationResult.cs#L31

Added line #L31 was not covered by tests
}
Expand Up @@ -27,4 +27,9 @@ public interface IDocumentClient
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>The document with the specified id if it exists; otherwise <see langword="null"/>.</returns>
Task<Document?> Get(int id, CancellationToken cancellationToken = default);

/// <summary>Creates a new document.</summary>
/// <param name="document">The document to create.</param>
/// <returns>Result of creating the document.</returns>
Task<DocumentCreationResult> Create(DocumentCreation document);
}
Expand Up @@ -8,6 +8,7 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization.Metadata;
using System.Threading;
using System.Threading.Tasks;

namespace VMelnalksnis.PaperlessDotNet.Serialization;

Expand Down Expand Up @@ -37,4 +38,15 @@ internal static class HttpClientExtensions
next = paginatedList.Next?.PathAndQuery;
}
}

internal static async Task EnsureSuccessStatusCodeAsync(this HttpResponseMessage response)
{
if (response.IsSuccessStatusCode)
{
return;
}

var message = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new HttpRequestException(message);

Check warning on line 50 in source/VMelnalksnis.PaperlessDotNet/Serialization/HttpClientExtensions.cs

View check run for this annotation

Codecov / codecov/patch

source/VMelnalksnis.PaperlessDotNet/Serialization/HttpClientExtensions.cs#L49-L50

Added lines #L49 - L50 were not covered by tests
}
}
Expand Up @@ -2,10 +2,12 @@
// Licensed under the Apache License 2.0.
// See LICENSE file in the project root for full license information.

using System.Collections.Generic;
using System.Text.Json.Serialization;

using VMelnalksnis.PaperlessDotNet.Correspondents;
using VMelnalksnis.PaperlessDotNet.Documents;
using VMelnalksnis.PaperlessDotNet.Tasks;

namespace VMelnalksnis.PaperlessDotNet.Serialization;

Expand All @@ -16,4 +18,5 @@ namespace VMelnalksnis.PaperlessDotNet.Serialization;
[JsonSerializable(typeof(CorrespondentCreation))]
[JsonSerializable(typeof(PaginatedList<Document>))]
[JsonSerializable(typeof(Document))]
[JsonSerializable(typeof(List<PaperlessTask>))]
internal partial class PaperlessJsonSerializerContext : JsonSerializerContext;
Expand Up @@ -12,6 +12,7 @@
using NodaTime.Serialization.SystemTextJson;

using VMelnalksnis.PaperlessDotNet.Correspondents;
using VMelnalksnis.PaperlessDotNet.Tasks;

namespace VMelnalksnis.PaperlessDotNet.Serialization;

Expand All @@ -26,6 +27,8 @@ public PaperlessJsonSerializerOptions(IDateTimeZoneProvider dateTimeZoneProvider
.ConfigureForNodaTime(dateTimeZoneProvider);

options.Converters.Add(new SmartEnumValueConverter<MatchingAlgorithm, int>());
options.Converters.Add(new SmartEnumNameConverter<PaperlessTaskStatus, int>());

options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;

Context = new(options);
Expand Down

0 comments on commit d6a90cc

Please sign in to comment.