Skip to content

Commit ccd3357

Browse files
authored
feat(utils): add JsonPathReader utility for JSON file path navigation (#841)
- Add JsonPathReader class for reading JSON files with property path navigation - Add JsonPathValue class with indexer and TryGetPathValue<T> support - Integrate with ImportDataViewModel for KonoAsset folder auto-detection - Add documentation in docs/11-json-utilities.md
1 parent 3cbf8a9 commit ccd3357

4 files changed

Lines changed: 410 additions & 1 deletion

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Nodes;
3+
4+
namespace AvatarExplorer.Core.Services.IO;
5+
6+
/// <summary>
7+
/// JSON ファイルを読み込み、プロパティを簡単に辿れる値として公開します。
8+
/// </summary>
9+
public sealed class JsonPathReader(string path)
10+
{
11+
/// <summary>このリーダーに関連付けられたパスを取得します。</summary>
12+
public string Path { get; } = path ?? throw new ArgumentNullException(nameof(path));
13+
14+
/// <summary>
15+
/// <see cref="Path"/> の JSON ファイルを読み込みます。
16+
/// </summary>
17+
/// <returns>読み込んだ値。不正な JSON の場合は null。</returns>
18+
public JsonPathValue? Read()
19+
{
20+
try
21+
{
22+
var node = JsonNode.Parse(File.ReadAllText(Path));
23+
return node is null ? null : new JsonPathValue(node);
24+
}
25+
catch (JsonException)
26+
{
27+
return null;
28+
}
29+
}
30+
}
31+
32+
/// <summary>
33+
/// JSON の値をプロパティパスで読み取るための薄いラッパーです。
34+
/// </summary>
35+
public sealed class JsonPathValue
36+
{
37+
private readonly JsonNode _node;
38+
39+
internal JsonPathValue(JsonNode node) => _node = node;
40+
41+
/// <summary>内部の <see cref="JsonNode"/> を取得します。</summary>
42+
public JsonNode Node => _node;
43+
44+
/// <summary>
45+
/// JSON オブジェクトのプロパティを取得します。値がプリミティブの場合は CLR 値を返します。
46+
/// </summary>
47+
/// <param name="propertyName">取得するプロパティ名。</param>
48+
/// <returns>プロパティ値、または存在しない場合は null。</returns>
49+
public dynamic? this[string propertyName]
50+
{
51+
get
52+
{
53+
if (_node is not JsonObject jsonObject || !jsonObject.TryGetPropertyValue(propertyName, out var value))
54+
{
55+
return null;
56+
}
57+
58+
return Wrap(value);
59+
}
60+
}
61+
62+
/// <summary>
63+
/// JSON 配列の要素を取得します。
64+
/// </summary>
65+
/// <param name="index">取得する配列インデックス。</param>
66+
/// <returns>配列要素、または範囲外の場合は null。</returns>
67+
public dynamic? this[int index]
68+
{
69+
get
70+
{
71+
if (_node is not JsonArray jsonArray || index < 0 || index >= jsonArray.Count)
72+
{
73+
return null;
74+
}
75+
76+
return Wrap(jsonArray[index]);
77+
}
78+
}
79+
80+
/// <summary>
81+
/// ドット区切りのプロパティパスから値を取得します。
82+
/// 配列は数値のパス要素(例: <c>items.0.name</c>)で指定できます。
83+
/// </summary>
84+
public bool TryGetPathValue<T>(string path, out T? result)
85+
{
86+
ArgumentNullException.ThrowIfNull(path);
87+
88+
JsonNode? current = _node;
89+
foreach (var part in path.Split('.', StringSplitOptions.RemoveEmptyEntries))
90+
{
91+
current = current switch
92+
{
93+
JsonObject obj when obj.TryGetPropertyValue(part, out var value) => value,
94+
JsonArray array when int.TryParse(part, out var index) && index >= 0 && index < array.Count => array[index],
95+
_ => null
96+
};
97+
98+
if (current is null)
99+
{
100+
result = default;
101+
return false;
102+
}
103+
}
104+
105+
try
106+
{
107+
result = current.Deserialize<T>(JsonManager.JsonSerializerOptions);
108+
return true;
109+
}
110+
catch (JsonException)
111+
{
112+
result = default;
113+
return false;
114+
}
115+
}
116+
117+
private static object? Wrap(JsonNode? node)
118+
{
119+
if (node is JsonObject or JsonArray)
120+
{
121+
return new JsonPathValue(node);
122+
}
123+
124+
if (node is not JsonValue value)
125+
{
126+
return null;
127+
}
128+
129+
using var document = JsonDocument.Parse(value.ToJsonString());
130+
var element = document.RootElement;
131+
return element.ValueKind switch
132+
{
133+
JsonValueKind.String => element.GetString(),
134+
JsonValueKind.Number when element.TryGetInt64(out var integer) => integer,
135+
JsonValueKind.Number when element.TryGetDecimal(out var decimalValue) => decimalValue,
136+
JsonValueKind.Number => element.GetDouble(),
137+
JsonValueKind.True => true,
138+
JsonValueKind.False => false,
139+
_ => null
140+
};
141+
}
142+
}

AvatarExplorer.UI/ViewModels/Overlays/ImportDataViewModel.cs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using AvatarExplorer.Core.Extensions;
44
using AvatarExplorer.Core.Localization;
55
using AvatarExplorer.Core.Models.External;
6+
using AvatarExplorer.Core.Services.IO;
67
using AvatarExplorer.UI.Interfaces;
78
using AvatarExplorer.UI.Localization;
89
using AvatarExplorer.UI.Services;
@@ -53,7 +54,18 @@ public ImportDataViewModel()
5354
public async Task Initialize()
5455
{
5556
this.WhenAnyValue(x => x.SelectedImportSourceIndex)
56-
.Subscribe(_ => CanImportThumbnails = SelectedImportSource != DataImportType.Folder);
57+
.Subscribe(_ =>
58+
{
59+
CanImportThumbnails = SelectedImportSource != DataImportType.Folder;
60+
if (SelectedImportSource == DataImportType.KonoAsset && TryGetKonoAssetFolderPath(out var path))
61+
{
62+
FolderPath = path;
63+
}
64+
else
65+
{
66+
FolderPath = string.Empty;
67+
}
68+
});
5769

5870
Localizer.Instance.LanguageChanged += OnLanguageChanged;
5971
OnLanguageChanged();
@@ -135,5 +147,27 @@ await NotificationManager.ShowWithProgress(
135147
}
136148
}
137149

150+
private static bool TryGetKonoAssetFolderPath(out string path)
151+
{
152+
path = string.Empty;
153+
154+
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
155+
var konoAssetFolderPath = Path.Combine(appDataPath, "dev.konoasset.app");
156+
var preferenceFilePath = Path.Combine(konoAssetFolderPath, "preference.json");
157+
158+
if (!File.Exists(preferenceFilePath)) return false;
159+
160+
var reader = new JsonPathReader(preferenceFilePath);
161+
var value = reader.Read();
162+
163+
if (value?.TryGetPathValue<string>("data.dataDirPath", out var dataDirPath) == true && !string.IsNullOrEmpty(dataDirPath))
164+
{
165+
path = dataDirPath;
166+
return true;
167+
}
168+
169+
return false;
170+
}
171+
138172
private void Close() => IsVisible = false;
139173
}

0 commit comments

Comments
 (0)