// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json; namespace EpicGames.Core { /// /// Exception thrown for errors parsing JSON files /// public class JsonParseException : Exception { /// /// Constructor /// /// Format string /// Optional arguments public JsonParseException(string format, params object[] args) : base(String.Format(format, args)) { } } /// /// Stores a JSON object in memory /// public class JsonObject { readonly Dictionary _rawObject; /// /// Construct a JSON object from the raw string -> object dictionary /// /// Raw object parsed from disk public JsonObject(Dictionary inRawObject) { _rawObject = new Dictionary(inRawObject, StringComparer.InvariantCultureIgnoreCase); } /// /// Constructor /// /// public JsonObject(JsonElement element) { _rawObject = new Dictionary(StringComparer.InvariantCultureIgnoreCase); foreach (JsonProperty property in element.EnumerateObject()) { _rawObject[property.Name] = ParseElement(property.Value); } } /// /// Parse an individual element /// /// /// public static object? ParseElement(JsonElement element) { switch(element.ValueKind) { case JsonValueKind.Array: return element.EnumerateArray().Select(x => ParseElement(x)).ToArray(); case JsonValueKind.Number: return element.GetDouble(); case JsonValueKind.Object: return element.EnumerateObject().ToDictionary(x => x.Name, x => ParseElement(x.Value)); case JsonValueKind.String: return element.GetString(); case JsonValueKind.False: return false; case JsonValueKind.True: return true; case JsonValueKind.Null: return null; default: throw new NotImplementedException(); } } /// /// Read a JSON file from disk and construct a JsonObject from it /// /// File to read from /// New JsonObject instance public static JsonObject Read(FileReference file) { string text = FileReference.ReadAllText(file); try { return Parse(text); } catch(Exception ex) { throw new JsonParseException("Unable to parse {0}: {1}", file, ex.Message); } } /// /// Tries to read a JSON file from disk /// /// File to read from /// On success, receives the parsed object /// True if the file was read, false otherwise public static bool TryRead(FileReference fileName, [NotNullWhen(true)] out JsonObject? result) { if (!FileReference.Exists(fileName)) { result = null; return false; } string text = FileReference.ReadAllText(fileName); return TryParse(text, out result); } /// /// Parse a JsonObject from the given raw text string /// /// The text to parse /// New JsonObject instance public static JsonObject Parse(string text) { JsonDocument document = JsonDocument.Parse(text, new JsonDocumentOptions { AllowTrailingCommas = true }); return new JsonObject(document.RootElement); } /// /// Try to parse a JsonObject from the given raw text string /// /// The text to parse /// On success, receives the new JsonObject /// True if the object was parsed public static bool TryParse(string text, [NotNullWhen(true)] out JsonObject? result) { try { result = Parse(text); return true; } catch (Exception) { result = null; return false; } } /// /// List of key names in this object /// public IEnumerable KeyNames => _rawObject.Keys; /// /// Gets a string field by the given name from the object, throwing an exception if it is not there or cannot be parsed. /// /// Name of the field to get /// The field value public string GetStringField(string fieldName) { string? stringValue; if (!TryGetStringField(fieldName, out stringValue)) { throw new JsonParseException("Missing or invalid '{0}' field", fieldName); } return stringValue; } /// /// Tries to read a string field by the given name from the object /// /// Name of the field to get /// On success, receives the field value /// True if the field could be read, false otherwise public bool TryGetStringField(string fieldName, [NotNullWhen(true)] out string? result) { object? rawValue; if (_rawObject.TryGetValue(fieldName, out rawValue) && (rawValue is string strValue)) { result = strValue; return true; } else { result = null; return false; } } /// /// Gets a string array field by the given name from the object, throwing an exception if it is not there or cannot be parsed. /// /// Name of the field to get /// The field value public string[] GetStringArrayField(string fieldName) { string[]? stringValues; if (!TryGetStringArrayField(fieldName, out stringValues)) { throw new JsonParseException("Missing or invalid '{0}' field", fieldName); } return stringValues; } /// /// Tries to read a string array field by the given name from the object /// /// Name of the field to get /// On success, receives the field value /// True if the field could be read, false otherwise public bool TryGetStringArrayField(string fieldName, [NotNullWhen(true)] out string[]? result) { object? rawValue; if (_rawObject.TryGetValue(fieldName, out rawValue) && (rawValue is IEnumerable enumValue) && enumValue.All(x => x is string)) { result = enumValue.Select(x => (string)x).ToArray(); return true; } else { result = null; return false; } } /// /// Gets a boolean field by the given name from the object, throwing an exception if it is not there or cannot be parsed. /// /// Name of the field to get /// The field value public bool GetBoolField(string fieldName) { bool boolValue; if (!TryGetBoolField(fieldName, out boolValue)) { throw new JsonParseException("Missing or invalid '{0}' field", fieldName); } return boolValue; } /// /// Tries to read a bool field by the given name from the object /// /// Name of the field to get /// On success, receives the field value /// True if the field could be read, false otherwise public bool TryGetBoolField(string fieldName, out bool result) { object? rawValue; if (_rawObject.TryGetValue(fieldName, out rawValue) && (rawValue is bool boolValue)) { result = boolValue; return true; } else { result = false; return false; } } /// /// Gets an integer field by the given name from the object, throwing an exception if it is not there or cannot be parsed. /// /// Name of the field to get /// The field value public int GetIntegerField(string fieldName) { int integerValue; if (!TryGetIntegerField(fieldName, out integerValue)) { throw new JsonParseException("Missing or invalid '{0}' field", fieldName); } return integerValue; } /// /// Tries to read an integer field by the given name from the object /// /// Name of the field to get /// On success, receives the field value /// True if the field could be read, false otherwise public bool TryGetIntegerField(string fieldName, out int result) { object? rawValue; if (!_rawObject.TryGetValue(fieldName, out rawValue) || !Int32.TryParse(rawValue?.ToString(), out result)) { result = 0; return false; } return true; } /// /// Tries to read an unsigned integer field by the given name from the object /// /// Name of the field to get /// On success, receives the field value /// True if the field could be read, false otherwise public bool TryGetUnsignedIntegerField(string fieldName, out uint result) { object? rawValue; if (!_rawObject.TryGetValue(fieldName, out rawValue) || !UInt32.TryParse(rawValue?.ToString(), out result)) { result = 0; return false; } return true; } /// /// Gets a double field by the given name from the object, throwing an exception if it is not there or cannot be parsed. /// /// Name of the field to get /// The field value public double GetDoubleField(string fieldName) { double doubleValue; if (!TryGetDoubleField(fieldName, out doubleValue)) { throw new JsonParseException("Missing or invalid '{0}' field", fieldName); } return doubleValue; } /// /// Tries to read a double field by the given name from the object /// /// Name of the field to get /// On success, receives the field value /// True if the field could be read, false otherwise public bool TryGetDoubleField(string fieldName, out double result) { object? rawValue; if (!_rawObject.TryGetValue(fieldName, out rawValue) || !Double.TryParse(rawValue?.ToString(), out result)) { result = 0.0; return false; } return true; } /// /// Gets an enum field by the given name from the object, throwing an exception if it is not there or cannot be parsed. /// /// Name of the field to get /// The field value public T GetEnumField(string fieldName) where T : struct { T enumValue; if (!TryGetEnumField(fieldName, out enumValue)) { throw new JsonParseException("Missing or invalid '{0}' field", fieldName); } return enumValue; } /// /// Tries to read an enum field by the given name from the object /// /// Name of the field to get /// On success, receives the field value /// True if the field could be read, false otherwise public bool TryGetEnumField(string fieldName, out T result) where T : struct { string? stringValue; if (!TryGetStringField(fieldName, out stringValue) || !Enum.TryParse(stringValue, true, out result)) { result = default; return false; } return true; } /// /// Tries to read an enum array field by the given name from the object /// /// Name of the field to get /// On success, receives the field value /// True if the field could be read, false otherwise public bool TryGetEnumArrayField(string fieldName, [NotNullWhen(true)] out T[]? result) where T : struct { string[]? stringValues; if (!TryGetStringArrayField(fieldName, out stringValues)) { result = null; return false; } T[] enumValues = new T[stringValues.Length]; for (int idx = 0; idx < stringValues.Length; idx++) { if (!Enum.TryParse(stringValues[idx], true, out enumValues[idx])) { result = null; return false; } } result = enumValues; return true; } /// /// Gets an object field by the given name from the object, throwing an exception if it is not there or cannot be parsed. /// /// Name of the field to get /// The field value public JsonObject GetObjectField(string fieldName) { JsonObject? result; if (!TryGetObjectField(fieldName, out result)) { throw new JsonParseException("Missing or invalid '{0}' field", fieldName); } return result; } /// /// Tries to read an object field by the given name from the object /// /// Name of the field to get /// On success, receives the field value /// True if the field could be read, false otherwise public bool TryGetObjectField(string fieldName, [NotNullWhen(true)] out JsonObject? result) { object? rawValue; if (_rawObject.TryGetValue(fieldName, out rawValue) && (rawValue is Dictionary dictValue)) { result = new JsonObject(dictValue); return true; } else { result = null; return false; } } /// /// Gets an object array field by the given name from the object, throwing an exception if it is not there or cannot be parsed. /// /// Name of the field to get /// The field value public JsonObject[] GetObjectArrayField(string fieldName) { JsonObject[]? result; if (!TryGetObjectArrayField(fieldName, out result)) { throw new JsonParseException("Missing or invalid '{0}' field", fieldName); } return result; } /// /// Tries to read an object array field by the given name from the object /// /// Name of the field to get /// On success, receives the field value /// True if the field could be read, false otherwise public bool TryGetObjectArrayField(string fieldName, [NotNullWhen(true)] out JsonObject[]? result) { object? rawValue; if (_rawObject.TryGetValue(fieldName, out rawValue) && (rawValue is IEnumerable enumValue) && enumValue.All(x => x is Dictionary)) { result = enumValue.Select(x => new JsonObject((Dictionary)x)).ToArray(); return true; } else { result = null; return false; } } } }