using System; using System.Collections.Generic; using System.IO; using Microsoft.Xna.Framework.Input; using Newtonsoft.Json; using StardewModdingAPI.Framework.Serialisation.CrossplatformConverters; using StardewModdingAPI.Framework.Serialisation.SmapiConverters; namespace StardewModdingAPI.Framework.Serialisation { /// Encapsulates SMAPI's JSON file parsing. internal class JsonHelper { /********* ** Accessors *********/ /// The JSON settings to use when serialising and deserialising files. private readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings { Formatting = Formatting.Indented, ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection values are duplicated each time the config is loaded Converters = new List { // enums new StringEnumConverter(), new StringEnumConverter(), new StringEnumConverter(), // crossplatform compatibility new ColorConverter(), new PointConverter(), new RectangleConverter() } }; /********* ** Public methods *********/ /// Read a JSON file. /// The model type. /// The absolete file path. /// Returns the deserialised model, or null if the file doesn't exist or is empty. /// The given path is empty or invalid. public TModel ReadJsonFile(string fullPath) where TModel : class { // validate if (string.IsNullOrWhiteSpace(fullPath)) throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath)); // read file string json; try { json = File.ReadAllText(fullPath); } catch (Exception ex) when (ex is DirectoryNotFoundException || ex is FileNotFoundException) { return null; } // deserialise model try { return this.Deserialise(json); } catch (Exception ex) { string error = $"Can't parse JSON file at {fullPath}."; if (ex is JsonReaderException) { error += " This doesn't seem to be valid JSON."; if (json.Contains("“") || json.Contains("”")) error += " Found curly quotes in the text; note that only straight quotes are allowed in JSON."; } error += $"\nTechnical details: {ex.Message}"; throw new JsonReaderException(error); } } /// Save to a JSON file. /// The model type. /// The absolete file path. /// The model to save. /// The given path is empty or invalid. public void WriteJsonFile(string fullPath, TModel model) where TModel : class { // validate if (string.IsNullOrWhiteSpace(fullPath)) throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath)); // create directory if needed string dir = Path.GetDirectoryName(fullPath); if (dir == null) throw new ArgumentException("The file path is invalid.", nameof(fullPath)); if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); // write file string json = JsonConvert.SerializeObject(model, this.JsonSettings); File.WriteAllText(fullPath, json); } /********* ** Private methods *********/ /// Deserialize JSON text if possible. /// The model type. /// The raw JSON text. private TModel Deserialise(string json) { try { return JsonConvert.DeserializeObject(json, this.JsonSettings); } catch (JsonReaderException) { // try replacing curly quotes if (json.Contains("“") || json.Contains("”")) { try { return JsonConvert.DeserializeObject(json.Replace('“', '"').Replace('”', '"'), this.JsonSettings); } catch { /* rethrow original error */ } } throw; } } } }