diff --git a/IPA.Loader/Config/Config.cs b/IPA.Loader/Config/Config.cs index ff9be00e..81517381 100644 --- a/IPA.Loader/Config/Config.cs +++ b/IPA.Loader/Config/Config.cs @@ -21,7 +21,7 @@ namespace IPA.Config { static Config() { - //JsonConfigProvider.RegisterConfig(); + JsonConfigProvider.RegisterConfig(); } /// diff --git a/IPA.Loader/Config/Data/Primitives.cs b/IPA.Loader/Config/Data/Primitives.cs index 23d4adcb..1d1bd813 100644 --- a/IPA.Loader/Config/Data/Primitives.cs +++ b/IPA.Loader/Config/Data/Primitives.cs @@ -35,6 +35,12 @@ namespace IPA.Config.Data /// public long Value { get; set; } + /// + /// Coerces this into a . + /// + /// a representing the closest approximation of + public FloatingPoint AsFloat() => Float(Value); + /// /// Converts this into a human-readable format. /// @@ -53,6 +59,12 @@ namespace IPA.Config.Data /// public double Value { get; set; } + /// + /// Coerces this into an . + /// + /// a representing the closest approximation of + public Integer AsInteger() => Integer((long)Value); + /// /// Converts this into a human-readable format. /// diff --git a/IPA.Loader/Config/Data/Value.cs b/IPA.Loader/Config/Data/Value.cs index a2abb681..a139dc27 100644 --- a/IPA.Loader/Config/Data/Value.cs +++ b/IPA.Loader/Config/Data/Value.cs @@ -38,6 +38,7 @@ namespace IPA.Config.Data /// /// an empty /// + /// public static Map Map() => new Map(); /// @@ -122,7 +123,18 @@ namespace IPA.Config.Data /// the dictionary of s to initialize the wtih /// a containing the content of /// - public static Map From(IDictionary vals) + /// + public static Map From(IDictionary vals) => From(vals as IEnumerable>); + + /// + /// Creates a new holding the content of an + /// of of to . + /// + /// the enumerable of of name to + /// a containing the content of + /// + /// + public static Map From(IEnumerable> vals) { if (vals == null) return null; var m = Map(); diff --git a/IPA.Loader/Config/IConfigProvider.cs b/IPA.Loader/Config/IConfigProvider.cs index 710145f6..823c7fb0 100644 --- a/IPA.Loader/Config/IConfigProvider.cs +++ b/IPA.Loader/Config/IConfigProvider.cs @@ -8,7 +8,17 @@ namespace IPA.Config /// An interface for configuration providers. /// /// + /// /// Implementers must provide a default constructor. Do not assume that will ever be set for a given object. + /// + /// + /// Implementers are expected to preserve the typing of values passed to when returned from . + /// The only exceptions to this are the numeric types, and , since they can be coerced + /// to each other with and respectively. The provider should + /// however store and recover with as much precision as is possible. For example, a JSON provider may decide to + /// decode all numbers that have an integral value, even if they were originally , as . + /// This is reasonable, as is more precise, particularly with larger values, than . + /// /// public interface IConfigProvider { diff --git a/IPA.Loader/Config/Providers/JsonConfigProvider.cs b/IPA.Loader/Config/Providers/JsonConfigProvider.cs index 1ef6e04a..e30dcbe2 100644 --- a/IPA.Loader/Config/Providers/JsonConfigProvider.cs +++ b/IPA.Loader/Config/Providers/JsonConfigProvider.cs @@ -1,14 +1,17 @@ -using IPA.Logging; +using IPA.Config.Data; +using IPA.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; +using System.Linq; using System.Collections.Specialized; using System.ComponentModel; using System.IO; +using System.Collections.Generic; +using Boolean = IPA.Config.Data.Boolean; namespace IPA.Config.Providers -{ // TODO: implement this for the new provider system - /*[Config.Type("json")] +{ internal class JsonConfigProvider : IConfigProvider { public static void RegisterConfig() @@ -16,112 +19,138 @@ namespace IPA.Config.Providers Config.Register(); } - private JObject jsonObj; + public string Extension => "json"; -#if NET4 - // TODO: create a wrapper that allows empty object creation - public dynamic Dynamic => jsonObj; -#endif + public FileInfo File { get; set; } - public bool HasChanged { get; private set; } - public bool InMemoryChanged { get; set; } - - public DateTime LastModified => File.GetLastWriteTime(Filename + ".json"); - - private string _filename; - - public string Filename + public Value Load() { - get => _filename; - set + if (!File.Exists) return Value.Null(); + + JToken jtok; + using (var sreader = new StreamReader(File.OpenRead())) { - if (_filename != null) - throw new InvalidOperationException("Can only assign to Filename once"); - _filename = value; + using var jreader = new JsonTextReader(sreader); + jtok = JToken.ReadFrom(jreader); } - } - - public void Load() - { - Logger.config.Debug($"Loading file {Filename}.json"); - var fileInfo = new FileInfo(Filename + ".json"); - if (fileInfo.Exists) + try { - string json = File.ReadAllText(fileInfo.FullName); - try - { - jsonObj = JObject.Parse(json); - } - catch (Exception e) - { - Logger.config.Error($"Error parsing JSON in file {Filename}.json; resetting to empty JSON"); - Logger.config.Error(e); - jsonObj = new JObject(); - File.WriteAllText(fileInfo.FullName, JsonConvert.SerializeObject(jsonObj, Formatting.Indented)); - } + return VisitToValue(jtok); } - else + catch (Exception e) { - jsonObj = new JObject(); + Logger.config.Error($"Error reading JSON file {File.FullName}; ignoring"); + Logger.config.Error(e); + return Value.Null(); } - - SetupListeners(); - InMemoryChanged = true; } - private void SetupListeners() + private Value VisitToValue(JToken tok) { - jsonObj.PropertyChanged += JsonObj_PropertyChanged; - jsonObj.ListChanged += JsonObj_ListChanged; -#if NET4 - jsonObj.CollectionChanged += JsonObj_CollectionChanged; -#endif - } + if (tok == null) return Value.Null(); -#if NET4 - private void JsonObj_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - HasChanged = true; - InMemoryChanged = true; - } -#endif - - private void JsonObj_ListChanged(object sender, ListChangedEventArgs e) - { - HasChanged = true; - InMemoryChanged = true; - } - - private void JsonObj_PropertyChanged(object sender, PropertyChangedEventArgs e) - { - HasChanged = true; - InMemoryChanged = true; + switch (tok.Type) + { + case JTokenType.Raw: // idk if the parser will normally emit a Raw type, but just to be safe + return VisitToValue(JToken.Parse((tok as JRaw).Value as string)); + case JTokenType.Undefined: + case JTokenType.Bytes: // never used by Newtonsoft + case JTokenType.Comment: // never used by Newtonsoft + case JTokenType.Constructor: // never used by Newtonsoft + case JTokenType.Property: // never used by Newtonsoft + case JTokenType.Null: + return Value.Null(); + case JTokenType.Boolean: + return Value.Bool(((tok as JValue).Value as bool?) ?? false); + case JTokenType.String: + var val = (tok as JValue).Value; + if (val is string s) return Value.Text(s); + else if (val is char c) return Value.Text("" + c); + else return Value.Text(string.Empty); + case JTokenType.Integer: + val = (tok as JValue).Value; + if (val is long l) return Value.Integer(l); + else if (val is ulong u) return Value.Integer((long)u); + else return Value.Integer(0); + case JTokenType.Float: + val = (tok as JValue).Value; + if (val is decimal dec) return Value.Float((double)dec); + else if (val is double dou) return Value.Float(dou); + else if (val is float flo) return Value.Float(flo); + else return Value.Float(0); // default to 0 if something breaks + case JTokenType.Date: + val = (tok as JValue).Value; + if (val is DateTime dt) return Value.Text(dt.ToString()); + else if (val is DateTimeOffset dto) return Value.Text(dto.ToString()); + else return Value.Text("Unknown Date-type token"); + case JTokenType.TimeSpan: + val = (tok as JValue).Value; + if (val is TimeSpan ts) return Value.Text(ts.ToString()); + else return Value.Text("Unknown TimeSpan-type token"); + case JTokenType.Guid: + val = (tok as JValue).Value; + if (val is Guid g) return Value.Text(g.ToString()); + else return Value.Text("Unknown Guid-type token"); + case JTokenType.Uri: + val = (tok as JValue).Value; + if (val is Uri ur) return Value.Text(ur.ToString()); + else return Value.Text("Unknown Uri-type token"); + case JTokenType.Array: + return Value.From((tok as JArray).Select(VisitToValue)); + case JTokenType.Object: + return Value.From((tok as IEnumerable>) + .Select(kvp => new KeyValuePair(kvp.Key, VisitToValue(kvp.Value)))); + default: + throw new ArgumentException($"Unknown {nameof(JTokenType)} in parameter"); + } } - public T Parse() + public void Store(Value value) { - if (jsonObj == null) - return default(T); - return jsonObj.ToObject(); - } + if (File.Directory.Exists) + File.Directory.Create(); - public void Save() - { - Logger.config.Debug($"Saving file {Filename}.json"); - if (!Directory.Exists(Path.GetDirectoryName(Filename))) - Directory.CreateDirectory(Path.GetDirectoryName(Filename) ?? throw new InvalidOperationException()); - File.WriteAllText(Filename + ".json", JsonConvert.SerializeObject(jsonObj, Formatting.Indented)); + try + { + var tok = VisitToToken(value); - HasChanged = false; + using var swriter = new StreamWriter(File.OpenWrite()); + using var jwriter = new JsonTextWriter(swriter); + tok.WriteTo(jwriter); + } + catch (Exception e) + { + Logger.config.Error($"Error serializing value for {File.FullName}"); + Logger.config.Error(e); + } } - public void Store(T obj) - { - jsonObj = JObject.FromObject(obj); - SetupListeners(); - HasChanged = true; - InMemoryChanged = true; + private JToken VisitToToken(Value val) + { + switch (val) + { + case Text t: + return new JValue(t.Value); + case Boolean b: + return new JValue(b.Value); + case Integer i: + return new JValue(i.Value); + case FloatingPoint f: + return new JValue(f.Value); + case List l: + var jarr = new JArray(); + foreach (var tok in l.Select(VisitToToken)) jarr.Add(tok); + return jarr; + case Map m: + var jobj = new JObject(); + foreach (var kvp in m) jobj.Add(kvp.Key, VisitToToken(kvp.Value)); + return jobj; + case null: + return JValue.CreateNull(); + default: + throw new ArgumentException($"Unsupported subtype of {nameof(Value)}"); + } } - }*/ + } } \ No newline at end of file diff --git a/IPA.Loader/Config/Stores/GeneratedStore.cs b/IPA.Loader/Config/Stores/GeneratedStore.cs index 0f4dd0d2..37e2ce7e 100644 --- a/IPA.Loader/Config/Stores/GeneratedStore.cs +++ b/IPA.Loader/Config/Stores/GeneratedStore.cs @@ -106,6 +106,7 @@ namespace IPA.Config.Stores { // TODO: implement Logger.config.Debug("Generated impl ReadFrom"); + Logger.config.Debug($"Read {provider.Load()}"); } internal static MethodInfo WriteToMethod = typeof(Impl).GetMethod(nameof(WriteTo));