From b76896dc7a2aefed83ea431126ebdeb105fd2c31 Mon Sep 17 00:00:00 2001 From: kryptomrx Date: Sat, 16 May 2026 23:20:21 +0200 Subject: [PATCH] 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) --- NozCli.Core/Client/NozApiClient.cs | 33 +++++++++++---------- NozCli.Core/Config/LocalConfig.cs | 21 ++++++------- NozCli.Core/Models/NoteInfo.cs | 15 +++++----- NozCli.Core/Models/NotesCacheFile.cs | 6 ++++ NozCli.Core/NozCli.Core.csproj | 3 -- NozCli.Core/Serialization/NozJsonContext.cs | 21 +++++++++++++ 6 files changed, 61 insertions(+), 38 deletions(-) create mode 100644 NozCli.Core/Models/NotesCacheFile.cs create mode 100644 NozCli.Core/Serialization/NozJsonContext.cs diff --git a/NozCli.Core/Client/NozApiClient.cs b/NozCli.Core/Client/NozApiClient.cs index 6a5a03d..7ff857d 100644 --- a/NozCli.Core/Client/NozApiClient.cs +++ b/NozCli.Core/Client/NozApiClient.cs @@ -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 GetConfigAsync() + public async Task GetConfigAsync(CancellationToken ct = default) { - var res = await _http.GetFromJsonAsync("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> GetNotesAsync() + public async Task> GetNotesAsync(CancellationToken ct = default) { - var res = await _http.GetFromJsonAsync("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 GetNoteAsync(string slug) + public async Task GetNoteAsync(string slug, CancellationToken ct = default) { - var res = await _http.GetFromJsonAsync($"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 PutNoteAsync(string slug, string markdown) + public async Task 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(); + 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(); } diff --git a/NozCli.Core/Config/LocalConfig.cs b/NozCli.Core/Config/LocalConfig.cs index 9678e0f..ad59a7a 100644 --- a/NozCli.Core/Config/LocalConfig.cs +++ b/NozCli.Core/Config/LocalConfig.cs @@ -1,13 +1,17 @@ using System.Text.Json; +using NozCli.Core.Serialization; namespace NozCli.Core.Config; public class LocalConfig { - public string ServerUrl { get; set; } = string.Empty; - public string Token { get; set; } = string.Empty; + 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(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, - }; } diff --git a/NozCli.Core/Models/NoteInfo.cs b/NozCli.Core/Models/NoteInfo.cs index 584288c..157b174 100644 --- a/NozCli.Core/Models/NoteInfo.cs +++ b/NozCli.Core/Models/NoteInfo.cs @@ -1,13 +1,14 @@ namespace NozCli.Core.Models; public record NoteInfo( - string Slug, - string Title, - string Status, - string Topic, - string Lang, - bool Private, - string? Hash + string Slug, + string Title, + string Status, + string Topic, + string Lang, + bool Private, + string? Hash, + string? Date = null ); public record NoteContent( diff --git a/NozCli.Core/Models/NotesCacheFile.cs b/NozCli.Core/Models/NotesCacheFile.cs new file mode 100644 index 0000000..fefdb58 --- /dev/null +++ b/NozCli.Core/Models/NotesCacheFile.cs @@ -0,0 +1,6 @@ +namespace NozCli.Core.Models; + +public record NotesCacheFile( + DateTimeOffset LastSynced, + List Notes +); diff --git a/NozCli.Core/NozCli.Core.csproj b/NozCli.Core/NozCli.Core.csproj index 758aaa1..37780e4 100644 --- a/NozCli.Core/NozCli.Core.csproj +++ b/NozCli.Core/NozCli.Core.csproj @@ -6,8 +6,5 @@ enable - - - diff --git a/NozCli.Core/Serialization/NozJsonContext.cs b/NozCli.Core/Serialization/NozJsonContext.cs new file mode 100644 index 0000000..1aadfc3 --- /dev/null +++ b/NozCli.Core/Serialization/NozJsonContext.cs @@ -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))] +[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);