diff --git a/Oma.WndwCtrl.Abstractions/ICommand.cs b/Oma.WndwCtrl.Abstractions/ICommand.cs index 771c22e..d317587 100644 --- a/Oma.WndwCtrl.Abstractions/ICommand.cs +++ b/Oma.WndwCtrl.Abstractions/ICommand.cs @@ -1,6 +1,8 @@ using System.Text.Json.Serialization; using JetBrains.Annotations; using LanguageExt; +using Oma.WndwCtrl.Abstractions.Model; +using ValueType = Oma.WndwCtrl.Abstractions.Model.ValueType; namespace Oma.WndwCtrl.Abstractions; @@ -17,8 +19,12 @@ public interface ICommand [JsonIgnore] string Category { get; } - IEnumerable Transformations { get; } + IList Transformations { get; } [JsonIgnore] Option Component { get; set; } + + ValueType InferredType => Transformations.LastOrDefault()?.ValueType ?? default; + + Cardinality InferredCardinality => Transformations.LastOrDefault()?.Cardinality ?? default; } \ No newline at end of file diff --git a/Oma.WndwCtrl.Abstractions/ITransformation.cs b/Oma.WndwCtrl.Abstractions/ITransformation.cs index 4df4985..03f696c 100644 --- a/Oma.WndwCtrl.Abstractions/ITransformation.cs +++ b/Oma.WndwCtrl.Abstractions/ITransformation.cs @@ -1,3 +1,11 @@ +using Oma.WndwCtrl.Abstractions.Model; +using ValueType = Oma.WndwCtrl.Abstractions.Model.ValueType; + namespace Oma.WndwCtrl.Abstractions; -public interface ITransformation; \ No newline at end of file +public interface ITransformation +{ + public Cardinality Cardinality { get; } + + public ValueType ValueType { get; } +} \ No newline at end of file diff --git a/Oma.WndwCtrl.Abstractions/Model/Cardinality.cs b/Oma.WndwCtrl.Abstractions/Model/Cardinality.cs index bcfe0a4..39a6e9d 100644 --- a/Oma.WndwCtrl.Abstractions/Model/Cardinality.cs +++ b/Oma.WndwCtrl.Abstractions/Model/Cardinality.cs @@ -5,6 +5,6 @@ namespace Oma.WndwCtrl.Abstractions.Model; [JsonConverter(typeof(JsonStringEnumConverter))] public enum Cardinality { - Single, - Multiple, + Single = 0, + Multiple = 1, } \ No newline at end of file diff --git a/Oma.WndwCtrl.Abstractions/Model/ValueType.cs b/Oma.WndwCtrl.Abstractions/Model/ValueType.cs index 6b066ff..0eb411e 100644 --- a/Oma.WndwCtrl.Abstractions/Model/ValueType.cs +++ b/Oma.WndwCtrl.Abstractions/Model/ValueType.cs @@ -5,8 +5,8 @@ namespace Oma.WndwCtrl.Abstractions.Model; [JsonConverter(typeof(JsonStringEnumConverter))] public enum ValueType { - Boolean, - String, - Long, - Decimal, + String = 0, + Boolean = 1, + Long = 2, + Decimal = 3, } \ No newline at end of file diff --git a/Oma.WndwCtrl.Api/CtrlApiService.cs b/Oma.WndwCtrl.Api/CtrlApiService.cs index 6c489a7..acc66fc 100644 --- a/Oma.WndwCtrl.Api/CtrlApiService.cs +++ b/Oma.WndwCtrl.Api/CtrlApiService.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; @@ -9,6 +10,7 @@ using Oma.WndwCtrl.Api.Hubs.MessageConsumer; using Oma.WndwCtrl.Api.OpenApi; using Oma.WndwCtrl.Configuration.Model; +using Oma.WndwCtrl.Core.Model.Settings; using Oma.WndwCtrl.CoreAsp; using Oma.WndwCtrl.Messaging.Bus; using Oma.WndwCtrl.Messaging.Extensions; @@ -44,8 +46,11 @@ protected override IServiceCollection ConfigureServices(IServiceCollection servi options => { options.AddDocumentTransformer( - (document, _, _) => + (document, context, _) => { + GeneralSettings generalSettings = context.ApplicationServices + .GetRequiredService>().Value; + document.Info = new OpenApiInfo { Title = "Component Control API", @@ -54,6 +59,12 @@ protected override IServiceCollection ConfigureServices(IServiceCollection servi Extensions = new Dictionary { ["acaad"] = new OpenApiString("commithash"), + ["acaad.metadata"] = new OpenApiObject + { + ["name"] = new OpenApiString(generalSettings.Name), + ["os"] = new OpenApiString(generalSettings.OS), + ["otlpEnabled"] = new OpenApiBoolean(generalSettings.UseOtlp), + }, }, }; diff --git a/Oma.WndwCtrl.Api/Oma.WndwCtrl.Api.csproj b/Oma.WndwCtrl.Api/Oma.WndwCtrl.Api.csproj index 3be25f2..e77ff8b 100644 --- a/Oma.WndwCtrl.Api/Oma.WndwCtrl.Api.csproj +++ b/Oma.WndwCtrl.Api/Oma.WndwCtrl.Api.csproj @@ -17,10 +17,6 @@ - - - - PreserveNewest diff --git a/Oma.WndwCtrl.Api/OpenApi/ComponentWriters/SensorComponentWriter.cs b/Oma.WndwCtrl.Api/OpenApi/ComponentWriters/SensorComponentWriter.cs index a4c9dc1..1311d97 100644 --- a/Oma.WndwCtrl.Api/OpenApi/ComponentWriters/SensorComponentWriter.cs +++ b/Oma.WndwCtrl.Api/OpenApi/ComponentWriters/SensorComponentWriter.cs @@ -1,4 +1,5 @@ using LanguageExt; +using Microsoft.OpenApi.Any; using Oma.WndwCtrl.Api.OpenApi.Interfaces; using Oma.WndwCtrl.Api.OpenApi.Model; using Oma.WndwCtrl.Core.Model; @@ -7,12 +8,25 @@ namespace Oma.WndwCtrl.Api.OpenApi.ComponentWriters; public class SensorComponentWriter(ILogger logger) : IOpenApiComponentWriter { - public Task> CreateExtensionAsync(Sensor component) => - Task.FromResult( + public Task> CreateExtensionAsync(Sensor component) + { + OpenApiComponentExtension componentExtension = new(component) + { + ["type"] = new OpenApiString(component.QueryCommand.InferredType.ToString()), + ["cardinality"] = new OpenApiString(component.QueryCommand.InferredCardinality.ToString()), + }; + + if (!string.IsNullOrEmpty(component.UnitOfMeasure)) + { + componentExtension["unitOfMeasure"] = new OpenApiString(component.UnitOfMeasure); + } + + return Task.FromResult( Option.Some( - new OpenApiComponentExtension(component) + componentExtension ) ); + } public ILogger Logger { get; } = logger; } \ No newline at end of file diff --git a/Oma.WndwCtrl.Api/OpenApi/ComponentWriters/SwitchComponentWriter.cs b/Oma.WndwCtrl.Api/OpenApi/ComponentWriters/SwitchComponentWriter.cs index 1427efc..d6ec4f2 100644 --- a/Oma.WndwCtrl.Api/OpenApi/ComponentWriters/SwitchComponentWriter.cs +++ b/Oma.WndwCtrl.Api/OpenApi/ComponentWriters/SwitchComponentWriter.cs @@ -1,4 +1,5 @@ using LanguageExt; +using Microsoft.OpenApi.Any; using Oma.WndwCtrl.Api.OpenApi.Interfaces; using Oma.WndwCtrl.Api.OpenApi.Model; using Oma.WndwCtrl.Core.Model; @@ -7,12 +8,32 @@ namespace Oma.WndwCtrl.Api.OpenApi.ComponentWriters; public class SwitchComponentWriter(ILogger logger) : IOpenApiComponentWriter { - public Task> CreateExtensionAsync(Switch component) => - Task.FromResult( + public Task> CreateExtensionAsync(Switch component) + { + OpenApiComponentExtension componentExtension = new(component) + { + ["onIff"] = MapOnIff(component), + }; + + return Task.FromResult( Option.Some( - new OpenApiComponentExtension(component) + componentExtension ) ); + } public ILogger Logger { get; } = logger; + + private static IOpenApiPrimitive MapOnIff(Switch component) + => component.OnIff switch + { + // TODO: Quite ugly to have different type mappings in Switch and here.. Oh well, fix later. + string s => new OpenApiString(s), + bool b => new OpenApiBoolean(b), + long l => new OpenApiLong(l), + decimal d => new OpenApiDouble((double)d), + var _ => throw new ArgumentException( + $"Unsupported type {component.OnIff.GetType()} for onIff (inferred value type: {component.InferredComparisonType})." + ), + }; } \ No newline at end of file diff --git a/Oma.WndwCtrl.Configuration/component-configuration-linux.json b/Oma.WndwCtrl.Configuration/component-configuration-linux.json index a52008d..cffa69b 100644 --- a/Oma.WndwCtrl.Configuration/component-configuration-linux.json +++ b/Oma.WndwCtrl.Configuration/component-configuration-linux.json @@ -65,6 +65,7 @@ }, "ping-google": { "type": "sensor", + "unitOfMeasure": "ms", "queryCommand": { "type": "cli", "fileName": "ping", diff --git a/Oma.WndwCtrl.Configuration/component-configuration-windows.json b/Oma.WndwCtrl.Configuration/component-configuration-windows.json index 04c86ed..508ede5 100644 --- a/Oma.WndwCtrl.Configuration/component-configuration-windows.json +++ b/Oma.WndwCtrl.Configuration/component-configuration-windows.json @@ -68,6 +68,7 @@ "ping-google": { "type": "sensor", "active": true, + "unitOfMeasure": "ms", "queryCommand": { "type": "cli", "fileName": "ping", @@ -132,6 +133,7 @@ "test-switch": { "type": "switch", "active": true, + "onIff": "RUNNING", "queryCommand": { "type": "cli", "fileName": "C:\\Program Files\\Git\\usr\\bin\\bash.exe", diff --git a/Oma.WndwCtrl.Core/Extensions/IServiceCollectionExtensions.cs b/Oma.WndwCtrl.Core/Extensions/IServiceCollectionExtensions.cs index 9e33868..4aaa41d 100644 --- a/Oma.WndwCtrl.Core/Extensions/IServiceCollectionExtensions.cs +++ b/Oma.WndwCtrl.Core/Extensions/IServiceCollectionExtensions.cs @@ -39,7 +39,7 @@ IConfiguration configuration .Configure( coreConfig.GetSection(CliParserLoggerOptions.SectionName) ) - .Configure(coreConfig.GetSection(GeneralSettings.SectionName)); + .Configure(configuration.GetSection(GeneralSettings.SectionName)); ExtensionSettings extensions = []; coreConfig.GetSection(ExtensionSettings.SectionName).Bind(extensions); diff --git a/Oma.WndwCtrl.Core/Model/Commands/BaseCommand.cs b/Oma.WndwCtrl.Core/Model/Commands/BaseCommand.cs index fa6f914..05cdf2f 100644 --- a/Oma.WndwCtrl.Core/Model/Commands/BaseCommand.cs +++ b/Oma.WndwCtrl.Core/Model/Commands/BaseCommand.cs @@ -15,7 +15,7 @@ public abstract class BaseCommand : ICommand [JsonIgnore] public abstract string Category { get; } - public IEnumerable Transformations { get; set; } = new List(); + public IList Transformations { get; set; } = new List(); [JsonIgnore] public Option Component { get; set; } diff --git a/Oma.WndwCtrl.Core/Model/Switch.cs b/Oma.WndwCtrl.Core/Model/Switch.cs index 187310c..8ef57f9 100644 --- a/Oma.WndwCtrl.Core/Model/Switch.cs +++ b/Oma.WndwCtrl.Core/Model/Switch.cs @@ -1,6 +1,8 @@ using System.Text.Json.Serialization; +using JetBrains.Annotations; using Oma.WndwCtrl.Abstractions; using Oma.WndwCtrl.Core.Interfaces; +using ValueType = Oma.WndwCtrl.Abstractions.Model.ValueType; namespace Oma.WndwCtrl.Core.Model; @@ -11,6 +13,20 @@ namespace Oma.WndwCtrl.Core.Model; /// public class Switch : Component, IStateQueryable { + public delegate (bool Is, object? instance) TryParse(object obj); + + [PublicAPI] + public static readonly IReadOnlyDictionary ValueTypeValidators = + new Dictionary + { + [ValueType.Boolean] = TryParseBool, + [ValueType.Long] = TryParseLong, + [ValueType.Decimal] = TryParseDecimal, + [ValueType.String] = TryParseString, + }; + + private object _onIff = true; + [JsonIgnore] public override string Type => "switch"; @@ -25,7 +41,98 @@ public class Switch : Component, IStateQueryable [JsonIgnore] public override IEnumerable Commands => [QueryCommand,]; + [JsonInclude] + public object OnIff + { + get => _onIff; + set + { + foreach ((ValueType valueType, TryParse validator) in ValueTypeValidators) + { + (bool typeMatch, object? instance) = validator(value); + + if (!typeMatch) + { + continue; + } + + InferredComparisonType = valueType; + _onIff = instance!; + break; + } + + if (InferredComparisonType is null) + { + throw new InvalidOperationException( + $"Unsupported type for OnIff: {value}. Allowed value types are [{string.Join(", ", ValueTypeValidators.Keys.Select(vt => vt.ToString()))}]" + ); + } + } + } + + [JsonIgnore] + public ValueType? InferredComparisonType { get; private set; } + [JsonInclude] [JsonRequired] public ICommand QueryCommand { get; internal set; } = null!; + + private static (bool, object?) TryParseBool(object obj) + { + if (obj is bool res) + { + return (true, res); + } + + if (bool.TryParse(obj.ToString(), out bool resParsed)) + { + return (true, resParsed); + } + + return (false, null); + } + + private static (bool, object?) TryParseString(object obj) + { + if (obj is string res) + { + return (true, res); + } + + string? asString = obj.ToString(); + + return !string.IsNullOrEmpty(asString) + ? (true, asString) + : (false, null); + } + + private static (bool, object?) TryParseLong(object obj) + { + if (obj is long res) + { + return (true, res); + } + + if (long.TryParse(obj.ToString(), out long resParsed)) + { + return (true, resParsed); + } + + return (false, null); + } + + private static (bool, object?) TryParseDecimal(object obj) + { + if (obj is decimal res) + { + return (true, res); + } + + if (decimal.TryParse(obj.ToString(), out decimal resParsed)) + { + return (true, resParsed); + } + + return (false, null); + } } \ No newline at end of file diff --git a/Oma.WndwCtrl.Core/Model/Transformations/BaseTransformation.cs b/Oma.WndwCtrl.Core/Model/Transformations/BaseTransformation.cs index 1b32e3a..4d3eda8 100644 --- a/Oma.WndwCtrl.Core/Model/Transformations/BaseTransformation.cs +++ b/Oma.WndwCtrl.Core/Model/Transformations/BaseTransformation.cs @@ -1,5 +1,11 @@ using Oma.WndwCtrl.Abstractions; +using Oma.WndwCtrl.Abstractions.Model; +using ValueType = Oma.WndwCtrl.Abstractions.Model.ValueType; namespace Oma.WndwCtrl.Core.Model.Transformations; -public record BaseTransformation : ITransformation; \ No newline at end of file +public record BaseTransformation : ITransformation +{ + public ValueType ValueType { get; init; } = ValueType.String; + public Cardinality Cardinality { get; init; } = Cardinality.Single; +} \ No newline at end of file diff --git a/Oma.WndwCtrl.Core/Model/Transformations/ParserTransformation.cs b/Oma.WndwCtrl.Core/Model/Transformations/ParserTransformation.cs index 9b378aa..31b6824 100644 --- a/Oma.WndwCtrl.Core/Model/Transformations/ParserTransformation.cs +++ b/Oma.WndwCtrl.Core/Model/Transformations/ParserTransformation.cs @@ -1,14 +1,7 @@ -using Oma.WndwCtrl.Abstractions.Model; -using ValueType = Oma.WndwCtrl.Abstractions.Model.ValueType; - namespace Oma.WndwCtrl.Core.Model.Transformations; [Serializable] public record ParserTransformation : BaseTransformation { public IReadOnlyList Statements { get; init; } = []; - - public Cardinality Cardinality { get; init; } = Cardinality.Single; - - public ValueType ValueType { get; init; } = ValueType.String; } \ No newline at end of file diff --git a/Oma.WndwCtrl.CoreAsp/BackgroundServiceWrapper.cs b/Oma.WndwCtrl.CoreAsp/BackgroundServiceWrapper.cs index 293a125..e107119 100644 --- a/Oma.WndwCtrl.CoreAsp/BackgroundServiceWrapper.cs +++ b/Oma.WndwCtrl.CoreAsp/BackgroundServiceWrapper.cs @@ -60,14 +60,18 @@ public async Task StartAsync(CancellationToken cancelToken = default, params str hostBuilder.ConfigureHostConfiguration(builder => builder.AddConfiguration(configuration)); hostBuilder.ConfigureLogging( - (_, logging) => + (_, lb) => { - logging.ClearProviders(); - logging.SetMinimumLevel(LogLevel.Trace); + lb.ClearProviders(); + lb.SetMinimumLevel(LogLevel.Trace); + +#if DEBUG + lb.AddConsole(); +#endif if (UseOtlp) { - logging.AddOpenTelemetry( + lb.AddOpenTelemetry( otelOptions => { ResourceBuilder resourceBuilder = @@ -89,7 +93,7 @@ public async Task StartAsync(CancellationToken cancelToken = default, params str ); } - logging.AddConfiguration(configuration.GetSection("Logging")); + lb.AddConfiguration(configuration.GetSection("Logging")); } ); diff --git a/Oma.WndwCtrl.CoreAsp/WebApplicationWrapper.cs b/Oma.WndwCtrl.CoreAsp/WebApplicationWrapper.cs index e282192..c1f1f80 100644 --- a/Oma.WndwCtrl.CoreAsp/WebApplicationWrapper.cs +++ b/Oma.WndwCtrl.CoreAsp/WebApplicationWrapper.cs @@ -141,6 +141,10 @@ public async Task StartAsync(CancellationToken cancelToken = default, params str { lb.ClearProviders(); +#if DEBUG + lb.AddConsole(); +#endif + if (UseOtlp) { lb.AddOpenTelemetry( diff --git a/Oma.WndwCtrl.MgmtApi/MgmtApiService.config.development.json b/Oma.WndwCtrl.MgmtApi/MgmtApiService.config.development.json index a2298bb..2664b35 100644 --- a/Oma.WndwCtrl.MgmtApi/MgmtApiService.config.development.json +++ b/Oma.WndwCtrl.MgmtApi/MgmtApiService.config.development.json @@ -1,7 +1,9 @@ { "Logging": { "LogLevel": { - "Default": "Trace" + "Default": "Information", + "Microsoft.AspNetCore.Hosting.Diagnostics": "Warning", + "Microsoft.AspNetCore.Routing.EndpointMiddleware": "Warning" } }, "Core": {