#nullable enable using System; using System.Collections.Generic; using UnityEngine; namespace ShrinkContext.AppAdapter { [Serializable] public sealed class ShrinkAppCompositionIsolate { public string key = string.Empty; public string realm = string.Empty; public ShrinkAppCompositionIsolate() { } public ShrinkAppCompositionIsolate(string key, string realm) { this.key = key; this.realm = realm; } internal ShrinkAppCompositionIsolate Clone() => new(key, realm); } [Serializable] public sealed class ShrinkAppCompositionMetadata { public string name = string.Empty; public string value = string.Empty; public ShrinkAppCompositionMetadata() { } public ShrinkAppCompositionMetadata(string name, string value) { this.name = name; this.value = value; } internal ShrinkAppCompositionMetadata Clone() => new(name, value); } [Serializable] public sealed class ShrinkAppCompositionIntercept { public string key = string.Empty; public List metadata = new(); public ShrinkAppCompositionIntercept() { } public ShrinkAppCompositionIntercept(string key, IEnumerable? metadata = null) { this.key = key; if (metadata != null) { foreach (var item in metadata) this.metadata.Add(item?.Clone() ?? throw new ArgumentException("Metadata must not contain null.")); } } internal ShrinkAppCompositionIntercept Clone() => new(key, metadata); } [Serializable] public sealed class ShrinkAppCompositionEntry { public string id = string.Empty; public bool enabled = true; public List isolate = new(); public List intercept = new(); public ShrinkAppCompositionEntry() { } public ShrinkAppCompositionEntry(string id, bool enabled = true, IEnumerable? isolate = null, IEnumerable? intercept = null) { this.id = id; this.enabled = enabled; if (isolate != null) { foreach (var item in isolate) this.isolate.Add(item?.Clone() ?? throw new ArgumentException("Isolate must not contain null.")); } if (intercept != null) { foreach (var item in intercept) this.intercept.Add(item?.Clone() ?? throw new ArgumentException("Intercept must not contain null.")); } } internal string NormalizedId => id?.Trim() ?? string.Empty; internal ShrinkAppCompositionEntry Clone() => new(id, enabled, isolate, intercept); internal IReadOnlyDictionary? BuildIsolate() { if (isolate == null || isolate.Count == 0) return null; var result = new Dictionary(StringComparer.Ordinal); foreach (var item in isolate) result.Add(item.key.Trim(), item.realm.Trim()); return result; } internal IReadOnlyDictionary>? BuildIntercept() { if (intercept == null || intercept.Count == 0) return null; var result = new Dictionary>(StringComparer.Ordinal); foreach (var item in intercept) { var values = new Dictionary(StringComparer.Ordinal); foreach (var pair in item.metadata) values.Add(pair.name.Trim(), pair.value); result.Add(item.key.Trim(), values); } return result; } } /// /// 可序列化的期望组合。组件工厂仍由代码目录注册;文档只控制条目选择、isolate 与 intercept。 /// intercept metadata 使用字符串值,领域策略负责解释其语义。 /// [Serializable] public sealed class ShrinkAppCompositionDocument { public bool includeUnlistedEntries = true; public List entries = new(); public ShrinkAppCompositionDocument() { } public ShrinkAppCompositionDocument(bool includeUnlistedEntries, IEnumerable? entries = null) { this.includeUnlistedEntries = includeUnlistedEntries; if (entries != null) { foreach (var entry in entries) this.entries.Add(entry?.Clone() ?? throw new ArgumentException("Entries must not contain null.")); } } public void Validate() { entries ??= new List(); var ids = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var entry in entries) { if (entry == null) throw new ShrinkLoaderException("Composition entries must not contain null."); if (string.IsNullOrWhiteSpace(entry.id)) throw new ShrinkLoaderException("Composition entry id must not be empty."); if (!ids.Add(entry.NormalizedId)) throw new ShrinkLoaderException($"Duplicate composition entry id: '{entry.NormalizedId}'."); ValidateIsolate(entry); ValidateIntercept(entry); } } public ShrinkAppCompositionDocument Clone() { Validate(); return new ShrinkAppCompositionDocument(includeUnlistedEntries, entries); } public string ToJson(bool prettyPrint = true) { Validate(); return JsonUtility.ToJson(this, prettyPrint); } public static ShrinkAppCompositionDocument FromJson(string json) { if (string.IsNullOrWhiteSpace(json)) throw new ArgumentException("Composition JSON must not be empty.", nameof(json)); var document = JsonUtility.FromJson(json); if (document == null) throw new ShrinkLoaderException("Composition JSON did not produce a document."); document.entries ??= new List(); document.Validate(); return document; } private static void ValidateIsolate(ShrinkAppCompositionEntry entry) { entry.isolate ??= new List(); var keys = new HashSet(StringComparer.Ordinal); foreach (var item in entry.isolate) { if (item == null || string.IsNullOrWhiteSpace(item.key) || string.IsNullOrWhiteSpace(item.realm)) throw new ShrinkLoaderException( $"Composition entry '{entry.NormalizedId}' has an empty isolate key or realm."); if (!keys.Add(item.key.Trim())) throw new ShrinkLoaderException( $"Composition entry '{entry.NormalizedId}' has duplicate isolate key '{item.key.Trim()}'."); } } private static void ValidateIntercept(ShrinkAppCompositionEntry entry) { entry.intercept ??= new List(); var keys = new HashSet(StringComparer.Ordinal); foreach (var item in entry.intercept) { if (item == null || string.IsNullOrWhiteSpace(item.key)) throw new ShrinkLoaderException( $"Composition entry '{entry.NormalizedId}' has an empty intercept key."); if (!keys.Add(item.key.Trim())) throw new ShrinkLoaderException( $"Composition entry '{entry.NormalizedId}' has duplicate intercept key '{item.key.Trim()}'."); item.metadata ??= new List(); var names = new HashSet(StringComparer.Ordinal); foreach (var pair in item.metadata) { if (pair == null || string.IsNullOrWhiteSpace(pair.name)) throw new ShrinkLoaderException( $"Composition entry '{entry.NormalizedId}' has empty intercept metadata."); if (!names.Add(pair.name.Trim())) throw new ShrinkLoaderException( $"Composition entry '{entry.NormalizedId}' has duplicate metadata '{pair.name.Trim()}'."); } } } } [CreateAssetMenu(fileName = DefaultResourceName, menuName = "ShrinkSDK/Cordis/Composition Profile")] public sealed class ShrinkAppCompositionProfile : ScriptableObject { public const string DefaultResourceName = "ShrinkAppComposition"; [SerializeField] private bool includeUnlistedEntries = true; [SerializeField] private List entries = new(); [Tooltip("设置后以该 TextAsset 的 JSON 文档为准;留空则使用上面的序列化条目。")] [SerializeField] private TextAsset? jsonOverride; public bool IncludeUnlistedEntries => includeUnlistedEntries; public IReadOnlyList Entries => entries ??= new List(); public TextAsset? JsonOverride => jsonOverride; public ShrinkAppCompositionDocument ResolveDocument() { if (jsonOverride != null && !string.IsNullOrWhiteSpace(jsonOverride.text)) return ShrinkAppCompositionDocument.FromJson(jsonOverride.text); return new ShrinkAppCompositionDocument(includeUnlistedEntries, entries); } public string ToJson(bool prettyPrint = true) => ResolveDocument().ToJson(prettyPrint); public void SetDocument(ShrinkAppCompositionDocument document) { var copy = (document ?? throw new ArgumentNullException(nameof(document))).Clone(); includeUnlistedEntries = copy.includeUnlistedEntries; entries = copy.entries; jsonOverride = null; } public void SetJsonOverride(TextAsset? value) => jsonOverride = value; public static ShrinkAppCompositionProfile? LoadDefault() => Resources.Load(DefaultResourceName); } }