diff --git a/TorchRemote.Models/Responses/LogLineResponse.cs b/TorchRemote.Models/Responses/LogLineResponse.cs index e7e5505..b909684 100644 --- a/TorchRemote.Models/Responses/LogLineResponse.cs +++ b/TorchRemote.Models/Responses/LogLineResponse.cs @@ -1,6 +1,6 @@ namespace TorchRemote.Models.Responses; -public record struct LogLineResponse(DateTime Time, LogLineLevel Level, string Logger, string Message); +public record LogLineResponse(DateTime Time, LogLineLevel Level, string Logger, string Message); public enum LogLineLevel { diff --git a/TorchRemote.Models/Responses/SettingInfoResponse.cs b/TorchRemote.Models/Responses/SettingInfoResponse.cs index 870dfe4..c5b8035 100644 --- a/TorchRemote.Models/Responses/SettingInfoResponse.cs +++ b/TorchRemote.Models/Responses/SettingInfoResponse.cs @@ -1,15 +1,6 @@ -namespace TorchRemote.Models.Responses; +using Json.Schema; -public record SettingInfoResponse(string Name, ICollection Properties); -public record SettingPropertyInfo(string Name, string? Description, int? Order, Guid Type); +namespace TorchRemote.Models.Responses; -public struct SettingPropertyTypeEnum -{ - public static readonly Guid Integer = new("95c0d25b-e44d-4505-9549-48ee9c14bce8"); - public static readonly Guid Boolean = new("028ef347-1fc3-486a-b70b-3d3b1dcdb538"); - public static readonly Guid Number = new("009ced71-4a69-4af0-abb9-ec3339fffce0"); - public static readonly Guid String = new("22dbed1b-b976-44b4-98c9-d1b742a93f0c"); - public static readonly Guid DateTime = new("f0978b29-9da9-4289-85c9-41d5b92056e8"); - public static readonly Guid TimeSpan = new("7a2bebf1-78f5-4e4e-8d83-18914dbee55c"); - public static readonly Guid Color = new("99c74632-0fa9-469b-ba05-825ba21a017b"); -} \ No newline at end of file +public record SettingInfoResponse(string Name, JsonSchema Schema, ICollection PropertyInfos); +public record SettingPropertyInfo(string Name, string PropertyName, string? Description, int? Order); \ No newline at end of file diff --git a/TorchRemote.Models/Shared/Settings/Property.cs b/TorchRemote.Models/Shared/Settings/Property.cs new file mode 100644 index 0000000..ef0b120 --- /dev/null +++ b/TorchRemote.Models/Shared/Settings/Property.cs @@ -0,0 +1,27 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace TorchRemote.Models.Shared.Settings; + +[JsonDerivedType(typeof(IntegerProperty), "integer")] +[JsonDerivedType(typeof(StringProperty), "string")] +[JsonDerivedType(typeof(BooleanProperty), "boolean")] +[JsonDerivedType(typeof(NumberProperty), "number")] +[JsonDerivedType(typeof(ObjectProperty), "object")] +[JsonDerivedType(typeof(EnumProperty), "enum")] +[JsonDerivedType(typeof(DateTimeProperty), "date-time")] +[JsonDerivedType(typeof(DurationProperty), "duration")] +[JsonDerivedType(typeof(UriProperty), "uri")] +[JsonDerivedType(typeof(UuidProperty), "uuid")] +public abstract record PropertyBase(string Name); + +public record IntegerProperty(string Name, int? Value) : PropertyBase(Name); +public record NumberProperty(string Name, double? Value) : PropertyBase(Name); +public record StringProperty(string Name, string? Value) : PropertyBase(Name); +public record EnumProperty(string Name, string Value) : PropertyBase(Name); +public record BooleanProperty(string Name, bool? Value) : PropertyBase(Name); +public record ObjectProperty(string Name, JsonElement Value) : PropertyBase(Name); +public record DateTimeProperty(string Name, DateTime? Value) : PropertyBase(Name); +public record DurationProperty(string Name, TimeSpan? Value) : PropertyBase(Name); +public record UriProperty(string Name, Uri? Value) : PropertyBase(Name); +public record UuidProperty(string Name, Guid? Value) : PropertyBase(Name); \ No newline at end of file diff --git a/TorchRemote.Models/TorchRemote.Models.csproj b/TorchRemote.Models/TorchRemote.Models.csproj index d28ab0c..838ba10 100644 --- a/TorchRemote.Models/TorchRemote.Models.csproj +++ b/TorchRemote.Models/TorchRemote.Models.csproj @@ -8,6 +8,8 @@ - + + + diff --git a/TorchRemote.Plugin/Abstractions/Controllers/ISettingsController.cs b/TorchRemote.Plugin/Abstractions/Controllers/ISettingsController.cs index bf54093..530d13f 100644 --- a/TorchRemote.Plugin/Abstractions/Controllers/ISettingsController.cs +++ b/TorchRemote.Plugin/Abstractions/Controllers/ISettingsController.cs @@ -3,5 +3,5 @@ namespace TorchRemote.Plugin.Abstractions.Controllers; public interface ISettingsController { - SettingInfoResponse Get(Guid id); + SettingInfoResponse Get(string fullName); } diff --git a/TorchRemote.Plugin/Controllers/SettingsController.cs b/TorchRemote.Plugin/Controllers/SettingsController.cs index f20066c..14c391f 100644 --- a/TorchRemote.Plugin/Controllers/SettingsController.cs +++ b/TorchRemote.Plugin/Controllers/SettingsController.cs @@ -1,25 +1,171 @@ -using EmbedIO; +using System.Reflection; +using System.Text.Json; +using EmbedIO; using EmbedIO.Routing; using EmbedIO.WebApi; -using Swan; +using Humanizer; +using Torch.Views; using TorchRemote.Models.Responses; +using TorchRemote.Models.Shared.Settings; using TorchRemote.Plugin.Abstractions.Controllers; using TorchRemote.Plugin.Utils; +using StringExtensions = Swan.StringExtensions; + namespace TorchRemote.Plugin.Controllers; public class SettingsController : WebApiController, ISettingsController { private const string RootPath = "/settings"; - [Route(HttpVerbs.Get, $"{RootPath}/{{id}}")] - public SettingInfoResponse Get(Guid id) + [Route(HttpVerbs.Get, $"{RootPath}/{{fullName}}")] + public SettingInfoResponse Get(string fullName) { - if (!Statics.SettingManager.Settings.TryGetValue(id, out var setting)) - throw HttpException.NotFound($"Setting with id {id} not found", id); + if (!Statics.SettingManager.Settings.TryGetValue(fullName, out var setting)) + throw HttpException.NotFound($"Setting with fullName {fullName} not found", fullName); - return new(setting.Name.Humanize(), setting.Properties.Select(b => - new SettingPropertyInfo(b.DisplayInfo?.Name ?? b.Name.Humanize(), - b.DisplayInfo?.Description, b.DisplayInfo?.Order, b.TypeId)) - .ToArray()); + return new(StringExtensions.Humanize(setting.Name), setting.Schema, setting + .Type.GetProperties( + BindingFlags.Public | BindingFlags.Instance) + .Where(b => setting.IncludeDisplayOnly && + b.HasAttribute()) + .Select(b => + { + var attr = b.GetCustomAttribute(); + return new SettingPropertyInfo( + attr?.Name ?? StringExtensions.Humanize(b.Name), + Statics.SerializerOptions + .PropertyNamingPolicy!.ConvertName(b.Name), + attr?.Description, + attr?.Order is 0 or null ? null : attr.Order); + }).ToArray()); + } + + [Route(HttpVerbs.Get, $"{RootPath}/{{fullName}}/values")] + public IEnumerable GetValues(string fullName, [JsonData] IEnumerable propertyNames) + { + if (!Statics.SettingManager.Settings.TryGetValue(fullName, out var setting)) + throw HttpException.NotFound($"Setting with fullName {fullName} not found", fullName); + + return propertyNames.Select(name => + { + var propInfo = + setting.Type.GetProperty(name.Pascalize(), BindingFlags.Instance | BindingFlags.Public); + + if (propInfo is null) + throw HttpException.NotFound("Property not found", name); + + var type = propInfo!.PropertyType; + var value = propInfo.GetValue(setting.Value); + + return type switch + { + _ when type == typeof(int) || type == typeof(int?) => (PropertyBase)new IntegerProperty(name, (int?)value), + _ when type == typeof(string) => new StringProperty(name, (string?)value), + _ when type.IsPrimitive => new NumberProperty(name, value is null ? null : (double?)Convert.ChangeType(value, typeof(double))), + _ when type == typeof(DateTime) || type == typeof(DateTime?) => new DateTimeProperty( + name, (DateTime?)value), + _ when type == typeof(TimeSpan) || type == typeof(TimeSpan?) => new DurationProperty( + name, (TimeSpan?)value), + _ when type.IsEnum || Nullable.GetUnderlyingType(type)?.IsEnum == true => new EnumProperty( + name, Enum.GetName(Nullable.GetUnderlyingType(type) ?? type, value)!), + _ when type == typeof(Guid) || type == typeof(Guid?) => new UuidProperty( + name, (Guid?)value), + _ when type == typeof(Uri) => new UriProperty(name, (Uri?)value), + _ when type.IsClass => new ObjectProperty( + name, JsonSerializer.SerializeToElement(value, type, Statics.SerializerOptions)), + _ => throw HttpException.NotFound("Property type not found", name), + }; + }); + } + + [Route(HttpVerbs.Patch, $"{RootPath}/{{fullName}}/values")] + public int Patch(string fullName, [JsonData] IEnumerable properties) + { + if (!Statics.SettingManager.Settings.TryGetValue(fullName, out var setting)) + throw HttpException.NotFound($"Setting with fullName {fullName} not found", fullName); + + return properties.Select(property => + { + void Throw() => throw HttpException.BadRequest("Invalid value", property); + + var propInfo = + setting.Type.GetProperty(property.Name.Pascalize(), BindingFlags.Instance | BindingFlags.Public); + + if (propInfo is null) Throw(); + + var type = propInfo!.PropertyType; + var instance = setting.Value; + + switch (property) + { + case BooleanProperty booleanProperty when type == typeof(bool): + if (booleanProperty.Value is null) Throw(); + propInfo.SetValue(instance, booleanProperty.Value!.Value); + break; + case BooleanProperty booleanProperty when type == typeof(bool?): + propInfo.SetValue(instance, booleanProperty.Value); + break; + case DateTimeProperty dateTimeProperty when type == typeof(DateTime): + if (dateTimeProperty.Value is null) Throw(); + propInfo.SetValue(instance, dateTimeProperty.Value!.Value); + break; + case DateTimeProperty dateTimeProperty when type == typeof(DateTime?): + propInfo.SetValue(instance, dateTimeProperty.Value); + break; + case DurationProperty durationProperty when type == typeof(TimeSpan): + if (durationProperty.Value is null) Throw(); + propInfo.SetValue(instance, durationProperty.Value!.Value); + break; + case DurationProperty durationProperty when type == typeof(TimeSpan?): + propInfo.SetValue(instance, durationProperty.Value); + break; + case EnumProperty enumProperty when type.IsEnum: + propInfo.SetValue(instance, Enum.Parse(type, enumProperty.Value, true)); + break; + case IntegerProperty integerProperty when type == typeof(int): + if (integerProperty.Value is null) Throw(); + propInfo.SetValue(instance, integerProperty.Value!.Value); + break; + case IntegerProperty integerProperty when type == typeof(int?): + propInfo.SetValue(instance, integerProperty.Value); + break; + case NumberProperty numberProperty when type.IsPrimitive: + if (numberProperty.Value is null) Throw(); + propInfo.SetValue( + instance, + type != typeof(double) + ? Convert.ChangeType(numberProperty.Value!.Value, type) + : numberProperty.Value!.Value); + break; + case NumberProperty numberProperty when Nullable.GetUnderlyingType(type)?.IsPrimitive == true: + propInfo.SetValue( + instance, + type != typeof(double?) + ? Convert.ChangeType(numberProperty.Value, type) + : numberProperty.Value); + break; + case ObjectProperty objectProperty when type.IsClass: + propInfo.SetValue(instance, objectProperty.Value.Deserialize(type, Statics.SerializerOptions)); + break; + case StringProperty stringProperty when type == typeof(string): + propInfo.SetValue(instance, stringProperty.Value); + break; + case UriProperty uriProperty when type == typeof(Uri): + propInfo.SetValue(instance, uriProperty.Value); + break; + case UuidProperty uuidProperty when type == typeof(Guid): + if (uuidProperty.Value is null) Throw(); + propInfo.SetValue(instance, uuidProperty.Value!.Value); + break; + case UuidProperty uuidProperty when type == typeof(Guid?): + propInfo.SetValue(instance, uuidProperty.Value); + break; + default: + Throw(); + break; + } + + return true; + }).Count(); } } diff --git a/TorchRemote.Plugin/Managers/SettingManager.cs b/TorchRemote.Plugin/Managers/SettingManager.cs index 20c71ef..f2fb34d 100644 --- a/TorchRemote.Plugin/Managers/SettingManager.cs +++ b/TorchRemote.Plugin/Managers/SettingManager.cs @@ -1,77 +1,50 @@ using System.Collections.Concurrent; -using System.Reflection; -using System.Text.Json.Serialization; -using System.Xml.Serialization; +using System.Text.Json; +using Json.Schema; +using Json.Schema.Generation; using NLog; using Torch.API; using Torch.Managers; -using Torch.Views; -using TorchRemote.Models.Responses; +using Torch.Server.Managers; +using Torch.Server.ViewModels; using TorchRemote.Plugin.Utils; -using VRage; + namespace TorchRemote.Plugin.Managers; public class SettingManager : Manager { private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); + + [Dependency] + private readonly InstanceManager _instanceManager = null!; public SettingManager(ITorchBase torchInstance) : base(torchInstance) { } - public Guid RegisterSetting(object value, Type type, bool includeOnlyDisplay = true) + public override void Attach() { - var properties = type.IsInterface ? type.GetProperties() : type.GetProperties(BindingFlags.Public | BindingFlags.Instance); - var settingProperties = properties - .Where(b => !b.HasAttribute() && - !b.HasAttribute() && - (!includeOnlyDisplay || - b.HasAttribute())) - .Select(property => new SettingProperty(property.Name, - GetTypeId(property.PropertyType, property.GetValue(value), includeOnlyDisplay), - property.PropertyType, property.GetMethod, property.SetMethod, - property.GetCustomAttribute() is { } attr ? - new(attr.Name, attr.Description, attr.GroupName, attr.Order, attr.ReadOnly, attr.Enabled) : - null)) - .ToArray(); - - var setting = new Setting(type.Name, type, settingProperties, value); - - var id = (type.FullName! + value.GetHashCode()).ToGuid(); - Settings.Add(id, setting); - Log.Debug("Registered type {0} with id {1}", type, id); - return id; + base.Attach(); + _instanceManager.InstanceLoaded += InstanceManagerOnInstanceLoaded; } - private Guid GetTypeId(Type type, object value, bool includeOnlyDisplay) + private void InstanceManagerOnInstanceLoaded(ConfigDedicatedViewModel config) { - if (type == typeof(int) || type == typeof(uint)) - return SettingPropertyTypeEnum.Integer; - if (type == typeof(bool)) - return SettingPropertyTypeEnum.Boolean; - if (type == typeof(short) || - type == typeof(ushort) || - type == typeof(byte) || - type == typeof(ulong) || - type == typeof(long) || - type == typeof(float) || - type == typeof(double) || - type == typeof(MyFixedPoint)) - return SettingPropertyTypeEnum.Number; - if (type == typeof(string)) - return SettingPropertyTypeEnum.String; - if (type == typeof(DateTime)) - return SettingPropertyTypeEnum.DateTime; - if (type == typeof(TimeSpan)) - return SettingPropertyTypeEnum.TimeSpan; - if (type == typeof(System.Drawing.Color) || type == typeof(VRageMath.Color)) - return SettingPropertyTypeEnum.Color; - return RegisterSetting(value, type, includeOnlyDisplay); + RegisterSetting(config.SessionSettings, typeof(SessionSettingsViewModel)); } - public IDictionary Settings { get; } = new ConcurrentDictionary(); + public void RegisterSetting(object value, Type type, bool includeOnlyDisplay = true) + { + var builder = new JsonSchemaBuilder().FromType(type, new() + { + PropertyNamingMethod = input => Statics.SerializerOptions.PropertyNamingPolicy!.ConvertName(input) + }); + + Settings[type.FullName!] = new(type.Name, type, builder.Build(), value, includeOnlyDisplay); + Log.Info("Registered {0} type", type.FullName); + } + + public IDictionary Settings { get; } = new ConcurrentDictionary(); } -public record Setting(string Name, Type Type, IEnumerable Properties, object? Value = null); -public record SettingProperty(string Name, Guid TypeId, Type Type, MethodInfo Getter, MethodInfo? Setter, SettingPropertyDisplayInfo? DisplayInfo); -public record SettingPropertyDisplayInfo(string? Name, string? Description, string? GroupName, int? Order, bool? IsReadOnly, bool? IsEnabled); +public record Setting(string Name, Type Type, JsonSchema Schema, object Value, bool IncludeDisplayOnly); diff --git a/TorchRemote.Plugin/TorchRemote.Plugin.csproj b/TorchRemote.Plugin/TorchRemote.Plugin.csproj index b45d623..3de5b18 100644 --- a/TorchRemote.Plugin/TorchRemote.Plugin.csproj +++ b/TorchRemote.Plugin/TorchRemote.Plugin.csproj @@ -77,9 +77,13 @@ - + + + + - + +