diff --git a/AdaptiveRemote.sln b/AdaptiveRemote.sln index a63a24a..c2ff52d 100644 --- a/AdaptiveRemote.sln +++ b/AdaptiveRemote.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 18.0.11217.181 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdaptiveRemote.App", "src\AdaptiveRemote.App\AdaptiveRemote.App.csproj", "{6C7C380B-D7A4-412E-8487-2AFC89EA802F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdaptiveRemote.Contracts", "src\AdaptiveRemote.Contracts\AdaptiveRemote.Contracts.csproj", "{F81FEF3B-DB7A-4C04-9DF0-72E98382097A}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdaptiveRemote", "src\AdaptiveRemote\AdaptiveRemote.csproj", "{7BE31162-0D09-4F80-8CE5-978F7AECC1EF}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdaptiveRemote.Console", "src\AdaptiveRemote.Console\AdaptiveRemote.Console.csproj", "{345B73FC-07F9-490F-B566-2677D10B1834}" @@ -188,6 +190,18 @@ Global {54522D5A-CEB3-F5B9-2654-1005EF1C3262}.Release|x64.Build.0 = Release|Any CPU {54522D5A-CEB3-F5B9-2654-1005EF1C3262}.Release|x86.ActiveCfg = Release|Any CPU {54522D5A-CEB3-F5B9-2654-1005EF1C3262}.Release|x86.Build.0 = Release|Any CPU + {F81FEF3B-DB7A-4C04-9DF0-72E98382097A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F81FEF3B-DB7A-4C04-9DF0-72E98382097A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F81FEF3B-DB7A-4C04-9DF0-72E98382097A}.Debug|x64.ActiveCfg = Debug|Any CPU + {F81FEF3B-DB7A-4C04-9DF0-72E98382097A}.Debug|x64.Build.0 = Debug|Any CPU + {F81FEF3B-DB7A-4C04-9DF0-72E98382097A}.Debug|x86.ActiveCfg = Debug|Any CPU + {F81FEF3B-DB7A-4C04-9DF0-72E98382097A}.Debug|x86.Build.0 = Debug|Any CPU + {F81FEF3B-DB7A-4C04-9DF0-72E98382097A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F81FEF3B-DB7A-4C04-9DF0-72E98382097A}.Release|Any CPU.Build.0 = Release|Any CPU + {F81FEF3B-DB7A-4C04-9DF0-72E98382097A}.Release|x64.ActiveCfg = Release|Any CPU + {F81FEF3B-DB7A-4C04-9DF0-72E98382097A}.Release|x64.Build.0 = Release|Any CPU + {F81FEF3B-DB7A-4C04-9DF0-72E98382097A}.Release|x86.ActiveCfg = Release|Any CPU + {F81FEF3B-DB7A-4C04-9DF0-72E98382097A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/backend.slnf b/backend.slnf new file mode 100644 index 0000000..bda5c53 --- /dev/null +++ b/backend.slnf @@ -0,0 +1,8 @@ +{ + "solution": { + "path": "AdaptiveRemote.sln", + "projects": [ + "src\\AdaptiveRemote.Contracts\\AdaptiveRemote.Contracts.csproj" + ] + } +} diff --git a/client.slnf b/client.slnf new file mode 100644 index 0000000..1991596 --- /dev/null +++ b/client.slnf @@ -0,0 +1,19 @@ +{ + "solution": { + "path": "AdaptiveRemote.sln", + "projects": [ + "src\\AdaptiveRemote.Contracts\\AdaptiveRemote.Contracts.csproj", + "src\\AdaptiveRemote.App\\AdaptiveRemote.App.csproj", + "src\\AdaptiveRemote\\AdaptiveRemote.csproj", + "src\\AdaptiveRemote.Console\\AdaptiveRemote.Console.csproj", + "src\\AdaptiveRemote.Headless\\AdaptiveRemote.Headless.csproj", + "test\\AdaptiveRemote.App.Tests\\AdaptiveRemote.App.Tests.csproj", + "test\\AdaptiveRemote.Speech.Tests\\AdaptiveRemote.Speech.Tests.csproj", + "test\\AdaptiveRemote.EndtoEndTests.TestServices\\AdaptiveRemote.EndtoEndTests.TestServices.csproj", + "test\\AdaptiveRemote.EndToEndTests.Steps\\AdaptiveRemote.EndToEndTests.Steps.csproj", + "test\\AdaptiveRemote.EndToEndTests.Host.Headless\\AdaptiveRemote.EndToEndTests.Host.Headless.csproj", + "test\\AdaptiveRemote.EndToEndTests.Host.Wpf\\AdaptiveRemote.EndToEndTests.Host.Wpf.csproj", + "test\\AdaptiveRemote.EndtoEndTests.Host.Console\\AdaptiveRemote.EndToEndTests.Host.Console.csproj" + ] + } +} diff --git a/src/AdaptiveRemote.App/AdaptiveRemote.App.csproj b/src/AdaptiveRemote.App/AdaptiveRemote.App.csproj index 396f43f..e104217 100644 --- a/src/AdaptiveRemote.App/AdaptiveRemote.App.csproj +++ b/src/AdaptiveRemote.App/AdaptiveRemote.App.csproj @@ -12,6 +12,10 @@ + + + + diff --git a/src/AdaptiveRemote.Contracts/AdaptiveRemote.Contracts.csproj b/src/AdaptiveRemote.Contracts/AdaptiveRemote.Contracts.csproj new file mode 100644 index 0000000..d804afe --- /dev/null +++ b/src/AdaptiveRemote.Contracts/AdaptiveRemote.Contracts.csproj @@ -0,0 +1,10 @@ + + + + net10.0 + enable + enable + AdaptiveRemote.Contracts + + + diff --git a/src/AdaptiveRemote.Contracts/CommandType.cs b/src/AdaptiveRemote.Contracts/CommandType.cs new file mode 100644 index 0000000..5ea8a4f --- /dev/null +++ b/src/AdaptiveRemote.Contracts/CommandType.cs @@ -0,0 +1,9 @@ +namespace AdaptiveRemote.Contracts; + +// Identifies the runtime command type. The client uses this to instantiate the correct +// App runtime type (TiVoCommand, IRCommand, LifecycleCommand). +// Type-specific execution parameters are resolved by the client from its own configuration: +// TiVo — CommandId = Name.ToUpperInvariant() (existing convention) +// IR — payload programmed via remote, stored in ProgrammaticSettings +// Others — keyed by Name +public enum CommandType { Lifecycle, TiVo, IR } diff --git a/src/AdaptiveRemote.Contracts/ICommandProperties.cs b/src/AdaptiveRemote.Contracts/ICommandProperties.cs new file mode 100644 index 0000000..51d7f27 --- /dev/null +++ b/src/AdaptiveRemote.Contracts/ICommandProperties.cs @@ -0,0 +1,14 @@ +namespace AdaptiveRemote.Contracts; + +// Shared behavioral interface — prevents drift between the compiled and raw command types. +// Adding a new behavioral property means updating this interface first; the compiler +// will flag any implementing record that doesn't follow. +public interface ICommandProperties +{ + CommandType Type { get; } + string Name { get; } + string Label { get; } + string? Glyph { get; } + string SpeakPhrase { get; } + string? Reverse { get; } +} diff --git a/src/AdaptiveRemote.Contracts/LayoutContractsJsonContext.cs b/src/AdaptiveRemote.Contracts/LayoutContractsJsonContext.cs new file mode 100644 index 0000000..1368475 --- /dev/null +++ b/src/AdaptiveRemote.Contracts/LayoutContractsJsonContext.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace AdaptiveRemote.Contracts; + +// Source-generated JSON context — required for Native AOT Lambda functions; +// shared by all consumers to ensure consistent serialization behaviour. +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +[JsonSerializable(typeof(RawLayout))] +[JsonSerializable(typeof(CompiledLayout))] +[JsonSerializable(typeof(PreviewLayout))] +[JsonSerializable(typeof(ValidationResult))] +[JsonSerializable(typeof(IReadOnlyList))] +[JsonSerializable(typeof(IReadOnlyList))] +public partial class LayoutContractsJsonContext : JsonSerializerContext { } diff --git a/src/AdaptiveRemote.Contracts/LayoutElementDto.cs b/src/AdaptiveRemote.Contracts/LayoutElementDto.cs new file mode 100644 index 0000000..92cf7d5 --- /dev/null +++ b/src/AdaptiveRemote.Contracts/LayoutElementDto.cs @@ -0,0 +1,53 @@ +using System.Text.Json.Serialization; + +namespace AdaptiveRemote.Contracts; + +// --------------------------------------------------------------------------- +// Compiled layout element DTOs +// Used in CompiledLayout.Elements. Deserialized directly by the client application. +// Contains only behavioral properties — grid positions and CSS overrides have been +// compiled into CssDefinitions and are not needed by the client. +// --------------------------------------------------------------------------- + +// "$type" avoids conflict with the behavioral Type property on CommandDefinitionDto. +[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] +[JsonDerivedType(typeof(CommandDefinitionDto), "command")] +[JsonDerivedType(typeof(LayoutGroupDefinitionDto), "group")] +public abstract record LayoutElementDto(string CssId); + +// Maps to AdaptiveRemote.App.Models.Command at layout-apply time (client epic). +// Type carries the CommandType discriminator so the client knows which runtime type to instantiate. +// No subtype hierarchy is used — all behavioral properties are flat; type-specific execution +// parameters are resolved by the client from its own configuration (see CommandType above). +public record CommandDefinitionDto( + CommandType Type, + string Name, + string Label, + string? Glyph, + string SpeakPhrase, + string? Reverse, + string CssId +) : LayoutElementDto(CssId), ICommandProperties; + +// Maps to AdaptiveRemote.App.Models.LayoutGroup at layout-apply time (client epic). +public record LayoutGroupDefinitionDto( + string CssId, + IReadOnlyList Children +) : LayoutElementDto(CssId); + +// --------------------------------------------------------------------------- +// Client-consumable format produced by LayoutCompilerService. +// Deserialized directly by the client application — no intermediate parsing model needed. +// The client maps Elements → runtime Command objects at layout-apply time (client epic). +// --------------------------------------------------------------------------- + +public record CompiledLayout( + Guid Id, + Guid RawLayoutId, + string UserId, + bool IsActive, + int Version, + IReadOnlyList Elements, + string CssDefinitions, // global CSS for the layout grid + DateTimeOffset CompiledAt +); diff --git a/src/AdaptiveRemote.Contracts/PreviewLayout.cs b/src/AdaptiveRemote.Contracts/PreviewLayout.cs new file mode 100644 index 0000000..2e968d2 --- /dev/null +++ b/src/AdaptiveRemote.Contracts/PreviewLayout.cs @@ -0,0 +1,11 @@ +namespace AdaptiveRemote.Contracts; + +// Editor-consumable preview format, produced by LayoutCompilerService. +public record PreviewLayout( + Guid RawLayoutId, + int Version, + string RenderedHtml, + string RenderedCss, + DateTimeOffset CompiledAt, + ValidationResult ValidationResult +); diff --git a/src/AdaptiveRemote.Contracts/RawLayoutElementDto.cs b/src/AdaptiveRemote.Contracts/RawLayoutElementDto.cs new file mode 100644 index 0000000..3bf30c8 --- /dev/null +++ b/src/AdaptiveRemote.Contracts/RawLayoutElementDto.cs @@ -0,0 +1,64 @@ +using System.Text.Json.Serialization; + +namespace AdaptiveRemote.Contracts; + +// --------------------------------------------------------------------------- +// Raw layout element DTOs +// Shared between the editor application (serialization) and LayoutCompilerService +// (deserialization). Extends behavioral properties with authoring properties that +// the compiler resolves into CssDefinitions and strips from the compiled output. +// --------------------------------------------------------------------------- + +// "$type" avoids conflict with the behavioral Type property on RawCommandDefinitionDto. +[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] +[JsonDerivedType(typeof(RawCommandDefinitionDto), "command")] +[JsonDerivedType(typeof(RawLayoutGroupDefinitionDto), "group")] +public abstract record RawLayoutElementDto( + string CssId, + int GridRow, + int GridColumn, + int GridRowSpan = 1, + int GridColumnSpan = 1, + string? AdditionalCss = null // per-element CSS overrides (e.g. red background for Power) +); + +public record RawCommandDefinitionDto( + CommandType Type, + string Name, + string Label, + string? Glyph, + string SpeakPhrase, + string? Reverse, + string CssId, + int GridRow, + int GridColumn, + int GridRowSpan = 1, + int GridColumnSpan = 1, + string? AdditionalCss = null +) : RawLayoutElementDto(CssId, GridRow, GridColumn, GridRowSpan, GridColumnSpan, AdditionalCss), + ICommandProperties; + +public record RawLayoutGroupDefinitionDto( + string CssId, + IReadOnlyList Children, + int GridRow, + int GridColumn, + int GridRowSpan = 1, + int GridColumnSpan = 1, + string? AdditionalCss = null +) : RawLayoutElementDto(CssId, GridRow, GridColumn, GridRowSpan, GridColumnSpan, AdditionalCss); + +// --------------------------------------------------------------------------- +// Administrator-editable source format. Elements are typed; no opaque JSON string. +// --------------------------------------------------------------------------- + +public record RawLayout( + Guid Id, + string UserId, + string Name, + IReadOnlyList Elements, + int Version, + DateTimeOffset CreatedAt, + DateTimeOffset UpdatedAt, + ValidationResult? ValidationResult // written by LayoutProcessingService via IRawLayoutStatusWriter +); diff --git a/src/AdaptiveRemote.Contracts/ValidationResult.cs b/src/AdaptiveRemote.Contracts/ValidationResult.cs new file mode 100644 index 0000000..81c3e7b --- /dev/null +++ b/src/AdaptiveRemote.Contracts/ValidationResult.cs @@ -0,0 +1,5 @@ +namespace AdaptiveRemote.Contracts; + +public record ValidationIssue(string Code, string Message, string? Path); + +public record ValidationResult(bool IsValid, IReadOnlyList Issues); diff --git a/src/_doc_Projects.md b/src/_doc_Projects.md index b786872..e81f19d 100644 --- a/src/_doc_Projects.md +++ b/src/_doc_Projects.md @@ -33,6 +33,14 @@ This document describes the high-level organization of the AdaptiveRemote reposi - Minimal code to launch the WPF app with console logging. - No business logic or features. +### AdaptiveRemote.Contracts +- **Purpose:** Shared class library containing layout definition DTOs, enums, interfaces, and the source-generated `LayoutContractsJsonContext` used by both the client application and backend services. +- **Guidance:** _No platform-specific dependencies._ Targets `net10.0` only. Contains pure data types (records, enums, interfaces) with no behavior. +- **Boundaries:** + - No WPF, Windows APIs, or Blazor dependencies. + - No MVVM or runtime behavior — DTOs only. + - Included in both `client.slnf` and `backend.slnf`. + ## Test Projects ### AdaptiveRemote.App.Tests