implement settings api

This commit is contained in:
zznty
2022-09-30 11:51:34 +07:00
parent 0ad21bda29
commit 7b448fe278
8 changed files with 225 additions and 82 deletions

View File

@@ -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
{

View File

@@ -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);

View 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);

View File

@@ -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>

View File

@@ -3,5 +3,5 @@ namespace TorchRemote.Plugin.Abstractions.Controllers;
public interface ISettingsController
{
SettingInfoResponse Get(Guid id);
SettingInfoResponse Get(string fullName);
}

View File

@@ -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();
}
}

View File

@@ -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 IDictionary<string, Setting> Settings { get; } = new ConcurrentDictionary<string, Setting>();
}
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 record Setting(string Name, Type Type, JsonSchema Schema, object Value, bool IncludeDisplayOnly);

View File

@@ -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>