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,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<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

@ -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(

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);