feat(api-client): add Yavsc.Api.Client with Blog + Circle + BlogAcl clients
Creates the high-level HTTP client library the PostIt UI will consume to manage blog posts, circles, and per-post ACLs. Clients in this commit: - BlogApiClient (moved from PostIt/Services; same public surface, now depends on IYavscApiClient instead of the concrete class). - CircleApiClient (new): GET/POST/PUT/DELETE /api/circle. Takes the blogs base URL explicitly in its constructor so it doesn't need to know about PostIt's Settings type. - BlogAclApiClient (new): GET/POST/PUT/DELETE /api/blogacl. Same conventions as CircleApiClient. DTOs (Yavsc.Api.Client.Dtos): - CircleDto: id, name, ownerId, public. Stops short of the navigation properties on the server-side Circle (Owner, Members), which depend on ApplicationUser and other server types we don't want to drag into the client. - CircleAuthorizationDto: circleId, blogPostId, comment. Same reason: the server entity has Target and Allowed navigation properties the client never needs. The clients now require the caller to pass the blogs base URL explicitly in the constructor (previously the BlogApiClient sniffed it off YavscApiClient.Settings.BlogsApiUrl, but that field is PostIt-specific). The one production call site (App.axaml.cs) and four test call sites are updated to pass the URL. Build + 51/51 tests green. The IYavscApiClient abstraction was landed in the previous commit so this one could be a pure addition + relocation.
This commit is contained in:
parent
ab40af8ef1
commit
f835ad42a1
11 changed files with 171 additions and 12 deletions
|
|
@ -7,6 +7,7 @@ using Avalonia.Controls.ApplicationLifetimes;
|
|||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Styling;
|
||||
using PostIt.Services;
|
||||
using Yavsc.Api.Client;
|
||||
using PostIt.ViewModels;
|
||||
using PostIt.Views;
|
||||
|
||||
|
|
@ -55,7 +56,7 @@ public partial class App : Application
|
|||
"PostIt", "tokens.json"));
|
||||
|
||||
var api = new YavscApiClient(settings, tokenStore);
|
||||
var client = new BlogApiClient(api);
|
||||
var client = new BlogApiClient(api, settings.BlogsApiUrl);
|
||||
|
||||
var services = new ServiceCollection();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,68 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Yavsc.Blogspot;
|
||||
|
||||
namespace PostIt.Services;
|
||||
|
||||
/// <summary>
|
||||
/// High-level client for the Blog subsystem of the Yavsc API
|
||||
/// (deployed at <c>https://blogs.pschneider.fr</c>). All transport
|
||||
/// concerns — base URL, JSON serialisation, Bearer auth, silent
|
||||
/// refresh on 401, request body shaping — are delegated to
|
||||
/// <see cref="YavscApiClient"/>. This class is a thin DTO↔path
|
||||
/// mapper, nothing more.
|
||||
///
|
||||
/// <para><b>URL convention.</b> <see cref="YavscApiClient"/>'s
|
||||
/// <c>BaseAddress</c> already terminates with <c>/api/v1/</c>
|
||||
/// (see <c>Settings.ApiUrl</c>). The path prefix below is
|
||||
/// therefore <i>relative</i> to that version segment: a prefix of
|
||||
/// <c>"blog"</c> resolves to <c>…/api/v1/blog</c>, which matches
|
||||
/// the <c>[Route(APIPrefix + "/blog")]</c> attribute on
|
||||
/// <c>Yavsc.Blogs.Controllers.BlogApiController</c>. Do not
|
||||
/// re-include the <c>api/</c> segment here — that produced 404s
|
||||
/// in the past (see commit "PostIt: fix blog API double-prefix").</para>
|
||||
///
|
||||
/// The class is intentionally non-IDisposable: it does not own the
|
||||
/// <see cref="YavscApiClient"/> it depends on. Lifetimes are managed
|
||||
/// by the consumer (typically a singleton service registered with
|
||||
/// the application).
|
||||
/// </summary>
|
||||
public sealed class BlogApiClient
|
||||
{
|
||||
private const string DefaultPathPrefix = "blog";
|
||||
|
||||
private readonly YavscApiClient _api;
|
||||
private readonly string _pathPrefix;
|
||||
|
||||
public BlogApiClient(YavscApiClient api, string pathPrefix = DefaultPathPrefix)
|
||||
{
|
||||
_api = api ?? throw new ArgumentNullException(nameof(api));
|
||||
|
||||
// ApiUrl is e.g. "https://blogs.pschneider.fr/api/v1/" — keep the
|
||||
// trailing slash so relative paths ("posts") resolve correctly.
|
||||
api.Http.BaseAddress = new Uri(api.Settings.BlogsApiUrl);
|
||||
|
||||
_pathPrefix = pathPrefix?.TrimStart('/') ?? DefaultPathPrefix;
|
||||
}
|
||||
|
||||
public Task<List<BlogPost>> GetPostsAsync(int start = 0, int take = 25, CancellationToken ct = default)
|
||||
=> _api.CallAsync<List<BlogPost>>(
|
||||
HttpMethod.Get,
|
||||
$"{_pathPrefix}?start={start}&take={take}",
|
||||
ct: ct);
|
||||
|
||||
public Task<BlogPost?> GetPostAsync(long id, CancellationToken ct = default)
|
||||
=> _api.CallAsync<BlogPost?>(HttpMethod.Get, $"{_pathPrefix}/{id}", ct: ct);
|
||||
|
||||
public Task<BlogPost?> CreatePostAsync(BlogPost post, CancellationToken ct = default)
|
||||
=> _api.CallAsync<BlogPost?>(HttpMethod.Post, _pathPrefix, body: post, ct: ct);
|
||||
|
||||
public Task UpdatePostAsync(long id, BlogPost post, CancellationToken ct = default)
|
||||
=> _api.CallAsync(HttpMethod.Put, $"{_pathPrefix}/{id}", body: post, ct: ct);
|
||||
|
||||
public Task DeletePostAsync(long id, CancellationToken ct = default)
|
||||
=> _api.CallAsync(HttpMethod.Delete, $"{_pathPrefix}/{id}", ct: ct);
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
|||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Yavsc.Blogspot;
|
||||
using Yavsc.Api.Client;
|
||||
using PostIt.Services;
|
||||
|
||||
namespace PostIt.ViewModels;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue