using System.Text.Json;
using System.Text.Json.Serialization;
using SheetMe.Core.Models;
namespace SheetMe.Core.Serialization;
///
/// JSON 직렬화기 — 부 포맷(파일 내보내기/백업, 추후 주 포맷 승격 대비).
/// 모델 트리를 있는 그대로 담아 레거시 XML 과 동일한 정보량을 유지한다.
///
public sealed class FormJsonSerializer : IDesignSerializer
{
#region Member Fields
private static readonly JsonSerializerOptions options = CreateOptions();
#endregion
#region Methods
/// JSON 을 서식 문서로 역직렬화
public FormDocument Read(string content)
=> JsonSerializer.Deserialize(content, options)
?? throw new InvalidDataException("JSON 서식 문서를 해석할 수 없습니다.");
/// 서식 문서를 JSON 으로 직렬화
public string Write(FormDocument document)
=> JsonSerializer.Serialize(document, options);
private static JsonSerializerOptions CreateOptions()
{
var opts = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
opts.Converters.Add(new PropBagJsonConverter());
opts.Converters.Add(new LegacyPropValueJsonConverter());
return opts;
}
#endregion
}
/// PropBag JSON 컨버터 — 순서 보존을 위해 [{ "n": 이름, "v": 값 }] 배열로 직렬화
public sealed class PropBagJsonConverter : JsonConverter
{
public override PropBag Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var bag = new PropBag();
if (reader.TokenType != JsonTokenType.StartArray)
{
throw new JsonException("PropBag 은 배열이어야 합니다.");
}
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException("PropBag 항목은 객체여야 합니다.");
}
string? name = null;
LegacyPropValue? value = null;
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
{
var propName = reader.GetString();
reader.Read();
switch (propName)
{
case "n":
name = reader.GetString();
break;
case "v":
value = JsonSerializer.Deserialize(ref reader, options);
break;
default:
reader.Skip();
break;
}
}
if (name is not null)
{
bag.Set(name, value ?? new LegacyPropValue.NullValue());
}
}
return bag;
}
public override void Write(Utf8JsonWriter writer, PropBag value, JsonSerializerOptions options)
{
writer.WriteStartArray();
foreach (var (name, propValue) in value)
{
writer.WriteStartObject();
writer.WriteString("n", name);
writer.WritePropertyName("v");
JsonSerializer.Serialize(writer, propValue, options);
writer.WriteEndObject();
}
writer.WriteEndArray();
}
}
/// LegacyPropValue 다형 JSON 컨버터 — "k" 판별자 기반
public sealed class LegacyPropValueJsonConverter : JsonConverter
{
public override LegacyPropValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return new LegacyPropValue.NullValue();
}
if (reader.TokenType == JsonTokenType.String)
{
// 축약형: 문자열이면 TextValue
return new LegacyPropValue.TextValue(reader.GetString() ?? string.Empty);
}
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException("LegacyPropValue 는 문자열 또는 객체여야 합니다.");
}
string? kind = null;
string? text = null;
string? base64 = null;
string? refName = null;
PropBag? nested = null;
List? items = null;
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
{
var propName = reader.GetString();
reader.Read();
switch (propName)
{
case "k": kind = reader.GetString(); break;
case "text": text = reader.GetString(); break;
case "b64": base64 = reader.GetString(); break;
case "ref": refName = reader.GetString(); break;
case "props": nested = JsonSerializer.Deserialize(ref reader, options); break;
case "items":
items = new List();
if (reader.TokenType != JsonTokenType.StartArray)
{
throw new JsonException("items 는 배열이어야 합니다.");
}
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
{
string? aqn = null;
LegacyPropValue? itemValue = null;
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
{
var itemProp = reader.GetString();
reader.Read();
switch (itemProp)
{
case "aqn": aqn = reader.GetString(); break;
case "v": itemValue = JsonSerializer.Deserialize(ref reader, options); break;
default: reader.Skip(); break;
}
}
items.Add(new LegacyItem { Aqn = aqn ?? string.Empty, Value = itemValue ?? new LegacyPropValue.NullValue() });
}
break;
default: reader.Skip(); break;
}
}
return kind switch
{
"text" => new LegacyPropValue.TextValue(text ?? string.Empty),
"null" => new LegacyPropValue.NullValue(),
"nested" => new LegacyPropValue.NestedValue { Children = nested ?? new PropBag() },
"items" => new LegacyPropValue.ItemsValue { Items = items ?? new List() },
"binary" => new LegacyPropValue.BinaryValue { Base64 = base64 ?? string.Empty },
"ref" => new LegacyPropValue.ReferenceValue { Name = refName ?? string.Empty },
_ => throw new JsonException($"알 수 없는 LegacyPropValue 종류: {kind}"),
};
}
public override void Write(Utf8JsonWriter writer, LegacyPropValue value, JsonSerializerOptions options)
{
switch (value)
{
case LegacyPropValue.TextValue text:
// 가장 흔한 형태는 축약(문자열)으로 — JSON 가독성
writer.WriteStringValue(text.Value);
break;
case LegacyPropValue.NullValue:
writer.WriteStartObject();
writer.WriteString("k", "null");
writer.WriteEndObject();
break;
case LegacyPropValue.NestedValue nested:
writer.WriteStartObject();
writer.WriteString("k", "nested");
writer.WritePropertyName("props");
JsonSerializer.Serialize(writer, nested.Children, options);
writer.WriteEndObject();
break;
case LegacyPropValue.ItemsValue items:
writer.WriteStartObject();
writer.WriteString("k", "items");
writer.WritePropertyName("items");
writer.WriteStartArray();
foreach (var item in items.Items)
{
writer.WriteStartObject();
writer.WriteString("aqn", item.Aqn);
writer.WritePropertyName("v");
JsonSerializer.Serialize(writer, item.Value, options);
writer.WriteEndObject();
}
writer.WriteEndArray();
writer.WriteEndObject();
break;
case LegacyPropValue.BinaryValue binary:
writer.WriteStartObject();
writer.WriteString("k", "binary");
writer.WriteString("b64", binary.Base64);
writer.WriteEndObject();
break;
case LegacyPropValue.ReferenceValue reference:
writer.WriteStartObject();
writer.WriteString("k", "ref");
writer.WriteString("ref", reference.Name);
writer.WriteEndObject();
break;
}
}
}