refactor(core): Native AOT - JSON source generators, AOT-safe models and API client

- Add NozJsonContext / NozJsonContextPretty source generator contexts
- Replace all reflection-based JSON with source-generated overloads
- Add NotesCacheFile record for persistent note cache
- Add date field to NoteInfo, CancellationToken to all API methods
- LocalConfig uses source-generated serialization (compact + indented)
This commit is contained in:
kryptomrx 2026-05-16 23:20:21 +02:00
parent 765ff7a976
commit b76896dc7a
6 changed files with 61 additions and 38 deletions

View file

@ -4,6 +4,7 @@ using System.Text;
using System.Text.Json;
using NozCli.Core.Config;
using NozCli.Core.Models;
using NozCli.Core.Serialization;
namespace NozCli.Core.Client;
@ -16,16 +17,15 @@ public class NozApiClient : IDisposable
_http = new HttpClient
{
BaseAddress = new Uri(cfg.ServerUrl.TrimEnd('/') + "/"),
// Enforce TLS — reject plain HTTP at the OS level
DefaultRequestVersion = new Version(2, 0),
};
_http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", cfg.Token);
}
public async Task<ServerCapabilities> GetConfigAsync()
public async Task<ServerCapabilities> GetConfigAsync(CancellationToken ct = default)
{
var res = await _http.GetFromJsonAsync<JsonElement>("api/cli/config");
var res = await _http.GetFromJsonAsync("api/cli/config", NozJsonContext.Default.JsonElement, ct);
return new ServerCapabilities(
Server: res.GetProperty("server").GetString()!,
Enabled: res.GetProperty("enabled").GetBoolean(),
@ -36,9 +36,9 @@ public class NozApiClient : IDisposable
);
}
public async Task<List<NoteInfo>> GetNotesAsync()
public async Task<List<NoteInfo>> GetNotesAsync(CancellationToken ct = default)
{
var res = await _http.GetFromJsonAsync<JsonElement>("api/cli/notes");
var res = await _http.GetFromJsonAsync("api/cli/notes", NozJsonContext.Default.JsonElement, ct);
var notes = res.GetProperty("notes");
return notes.EnumerateArray().Select(n => new NoteInfo(
Slug: n.GetProperty("slug").GetString()!,
@ -47,13 +47,14 @@ public class NozApiClient : IDisposable
Topic: n.GetProperty("topic").GetString()!,
Lang: n.GetProperty("lang").GetString()!,
Private: n.GetProperty("private").GetBoolean(),
Hash: n.TryGetProperty("hash", out var h) ? h.GetString() : null
Hash: n.TryGetProperty("hash", out var h) ? h.GetString() : null,
Date: n.TryGetProperty("date", out var d) ? d.GetString() : null
)).ToList();
}
public async Task<NoteContent> GetNoteAsync(string slug)
public async Task<NoteContent> GetNoteAsync(string slug, CancellationToken ct = default)
{
var res = await _http.GetFromJsonAsync<JsonElement>($"api/cli/notes/{slug}");
var res = await _http.GetFromJsonAsync($"api/cli/notes/{slug}", NozJsonContext.Default.JsonElement, ct);
return new NoteContent(
Slug: res.GetProperty("slug").GetString()!,
Markdown: res.GetProperty("markdown").GetString()!,
@ -61,25 +62,25 @@ public class NozApiClient : IDisposable
);
}
public async Task<string> PutNoteAsync(string slug, string markdown)
public async Task<string> PutNoteAsync(string slug, string markdown, CancellationToken ct = default)
{
var body = JsonSerializer.Serialize(new { markdown });
var body = JsonSerializer.Serialize(new PushBody(markdown), NozJsonContext.Default.PushBody);
var content = new StringContent(body, Encoding.UTF8, "application/json");
var res = await _http.PutAsync($"api/cli/notes/{slug}", content);
var res = await _http.PutAsync($"api/cli/notes/{slug}", content, ct);
res.EnsureSuccessStatusCode();
var json = await res.Content.ReadFromJsonAsync<JsonElement>();
var json = await res.Content.ReadFromJsonAsync(NozJsonContext.Default.JsonElement, ct);
return json.GetProperty("hash").GetString()!;
}
public async Task DeleteNoteAsync(string slug)
public async Task DeleteNoteAsync(string slug, CancellationToken ct = default)
{
var res = await _http.DeleteAsync($"api/cli/notes/{slug}");
var res = await _http.DeleteAsync($"api/cli/notes/{slug}", ct);
res.EnsureSuccessStatusCode();
}
public async Task RebuildIndexAsync()
public async Task RebuildIndexAsync(CancellationToken ct = default)
{
var res = await _http.PostAsync("api/cli/rebuild", null);
var res = await _http.PostAsync("api/cli/rebuild", null, ct);
res.EnsureSuccessStatusCode();
}

View file

@ -1,4 +1,5 @@
using System.Text.Json;
using NozCli.Core.Serialization;
namespace NozCli.Core.Config;
@ -6,8 +7,11 @@ public class LocalConfig
{
public string ServerUrl { get; set; } = string.Empty;
public string Token { get; set; } = string.Empty;
/// Default editor (nano, vim, vi, notepad …) — used by 'edit' and 'new'
public string? Editor { get; set; } = null;
/// Editor for the -c flag (code, zed, kate …) — used by 'edit -c' and 'new -c'
public string? CodeEditor { get; set; } = null;
// ~/.config/noz/config.json
private static readonly string ConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".config", "noz", "config.json"
@ -17,24 +21,17 @@ public class LocalConfig
{
if (!File.Exists(ConfigPath)) return null;
var json = File.ReadAllText(ConfigPath);
return JsonSerializer.Deserialize<LocalConfig>(json, JsonOptions);
return JsonSerializer.Deserialize(json, NozJsonContext.Default.LocalConfig);
}
public void Save()
{
Directory.CreateDirectory(Path.GetDirectoryName(ConfigPath)!);
// 0600 — only owner can read
var json = JsonSerializer.Serialize(this, JsonOptions);
var json = JsonSerializer.Serialize(this, NozJsonContextPretty.Default.LocalConfig);
File.WriteAllText(ConfigPath, json);
if (!OperatingSystem.IsWindows())
File.SetUnixFileMode(ConfigPath, UnixFileMode.UserRead | UnixFileMode.UserWrite);
}
public static bool Exists() => File.Exists(ConfigPath);
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
};
}

View file

@ -7,7 +7,8 @@ public record NoteInfo(
string Topic,
string Lang,
bool Private,
string? Hash
string? Hash,
string? Date = null
);
public record NoteContent(

View file

@ -0,0 +1,6 @@
namespace NozCli.Core.Models;
public record NotesCacheFile(
DateTimeOffset LastSynced,
List<NoteInfo> Notes
);

View file

@ -6,8 +6,5 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" Version="10.0.8" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,21 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using NozCli.Core.Config;
using NozCli.Core.Models;
namespace NozCli.Core.Serialization;
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower)]
[JsonSerializable(typeof(LocalConfig))]
[JsonSerializable(typeof(PushBody))]
[JsonSerializable(typeof(JsonElement))]
[JsonSerializable(typeof(NotesCacheFile))]
[JsonSerializable(typeof(List<NoteInfo>))]
[JsonSerializable(typeof(NoteInfo))]
public partial class NozJsonContext : JsonSerializerContext { }
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower, WriteIndented = true)]
[JsonSerializable(typeof(LocalConfig))]
internal partial class NozJsonContextPretty : JsonSerializerContext { }
public record PushBody(string Markdown);