implement settings api
This commit is contained in:
@@ -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
|
||||
{
|
||||
|
@@ -1,15 +1,6 @@
|
||||
namespace TorchRemote.Models.Responses;
|
||||
using Json.Schema;
|
||||
|
||||
public record SettingInfoResponse(string Name, ICollection<SettingPropertyInfo> 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");
|
||||
}
|
||||
public record SettingInfoResponse(string Name, JsonSchema Schema, ICollection<SettingPropertyInfo> PropertyInfos);
|
||||
public record SettingPropertyInfo(string Name, string PropertyName, string? Description, int? Order);
|
27
TorchRemote.Models/Shared/Settings/Property.cs
Normal file
27
TorchRemote.Models/Shared/Settings/Property.cs
Normal file
@@ -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);
|
@@ -8,6 +8,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Text.Json" Version="7.0.0-preview.6.22324.4" />
|
||||
<PackageReference Include="Json.More.Net" Version="1.7.1" />
|
||||
<PackageReference Include="JsonSchema.Net" Version="3.2.1" />
|
||||
<PackageReference Include="System.Text.Json" Version="7.0.0-rc.1.22426.10" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@@ -3,5 +3,5 @@ namespace TorchRemote.Plugin.Abstractions.Controllers;
|
||||
|
||||
public interface ISettingsController
|
||||
{
|
||||
SettingInfoResponse Get(Guid id);
|
||||
SettingInfoResponse Get(string fullName);
|
||||
}
|
||||
|
@@ -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<DisplayAttribute>())
|
||||
.Select(b =>
|
||||
{
|
||||
var attr = b.GetCustomAttribute<DisplayAttribute>();
|
||||
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<PropertyBase> GetValues(string fullName, [JsonData] IEnumerable<string> 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<PropertyBase> 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();
|
||||
}
|
||||
}
|
||||
|
@@ -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<XmlIgnoreAttribute>() &&
|
||||
!b.HasAttribute<JsonIgnoreAttribute>() &&
|
||||
(!includeOnlyDisplay ||
|
||||
b.HasAttribute<DisplayAttribute>()))
|
||||
.Select(property => new SettingProperty(property.Name,
|
||||
GetTypeId(property.PropertyType, property.GetValue(value), includeOnlyDisplay),
|
||||
property.PropertyType, property.GetMethod, property.SetMethod,
|
||||
property.GetCustomAttribute<DisplayAttribute>() 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<Guid, Setting> Settings { get; } = new ConcurrentDictionary<Guid, Setting>();
|
||||
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 record Setting(string Name, Type Type, IEnumerable<SettingProperty> 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 IDictionary<string, Setting> Settings { get; } = new ConcurrentDictionary<string, Setting>();
|
||||
}
|
||||
|
||||
public record Setting(string Name, Type Type, JsonSchema Schema, object Value, bool IncludeDisplayOnly);
|
||||
|
@@ -77,9 +77,13 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="EmbedIO" Version="3.4.3" />
|
||||
<PackageReference Include="EmbedIO.BearerToken" Version="3.4.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" ExcludeAssets="all" PrivateAssets="all" />
|
||||
<PackageReference Include="Json.More.Net" Version="1.7.1" />
|
||||
<PackageReference Include="JsonPointer.Net" Version="2.2.2" />
|
||||
<PackageReference Include="JsonSchema.Net" Version="3.2.2" />
|
||||
<PackageReference Include="JsonSchema.Net.Generation" Version="3.0.4" />
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="4.0.0" PrivateAssets="all" />
|
||||
<PackageReference Include="System.Text.Json" Version="7.0.0-preview.6.22324.4" />
|
||||
<PackageReference Include="System.Text.Json" Version="7.0.0-rc.1.22426.10" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" ExcludeAssets="all" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
Reference in New Issue
Block a user