Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fd966e4
chore: add net10 multi-targeting and refresh packages/readme
Agash Feb 24, 2026
8c29e11
chore: upgrade to Roslyn 5 and MSTest 4.1
Agash Feb 24, 2026
46e0eb9
feat: prep native-aot
Agash Feb 24, 2026
5fd1757
initial aot pass
Agash Feb 24, 2026
e9e78e1
feat(codegen): replace Roslyn source generator with build-time codege…
Agash Feb 24, 2026
145173f
feat(core): make JSON and MsgPack runtime paths AOT-oriented
Agash Feb 24, 2026
3d797ae
test(core): expand serializer and client coverage for AOT serializati…
Agash Feb 24, 2026
5fe511e
docs(example): update README and modernize sample app for JSON/MsgPac…
Agash Feb 24, 2026
6e7b8c0
test+ci(serialization): add JSON/MsgPack matrix coverage and AOT smok…
Agash Feb 24, 2026
e6766d2
fix(msgpack): parse incoming OBS envelope payload as raw map bytes
Agash Feb 24, 2026
98b6bc7
fix(msgpack): preserve nested payload bytes in wrapper deserialization
Agash Feb 24, 2026
8d98ba0
fix(example): make CustomEvent transport validation robust to payload…
Agash Feb 24, 2026
f84cf75
Finalize AOT serializers, MsgPack runtime fixes, and Spectre example UX
Agash Feb 24, 2026
54e6cbf
Refactor MsgPack resolver generation to native source-generated deleg…
Agash Feb 24, 2026
eced504
Fix MsgPack AOT envelope serialization and generic SG conflicts
Agash Feb 24, 2026
ba02502
Harden batch AOT request-data path and tighten serializer test coverage
Agash Feb 24, 2026
1a86262
Add one-shot example mode and stabilize MsgPack AOT transport paths
Agash Feb 24, 2026
f9c8688
Fix CI AOT publish target and restore codegen warnings
Agash Feb 24, 2026
a756cbf
Fix AOT smoke publish restore alignment in CI
Agash Feb 24, 2026
5964529
Harden AOT publish and extend transport extension-data validation
Agash Feb 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
24 changes: 22 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,37 @@ jobs:
with:
fetch-depth: 0 # Required for MinVer to determine the version from Git history

- name: Setup .NET 9 SDK
- name: Setup .NET 10 SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.x' # Use the specific .NET 9 SDK
dotnet-version: '10.x'

- name: Restore dependencies
run: dotnet restore ObsWebSocket.sln

- name: Build Solution
run: dotnet build ObsWebSocket.sln --configuration Release --no-restore

- name: Restore Example for AOT RID graph
run: >
dotnet restore ObsWebSocket.Example/ObsWebSocket.Example.csproj
-p:TargetFramework=net10.0
--runtime linux-x64
-p:SelfContained=true
-p:PublishAot=true

- name: Native AOT Smoke Publish (ObsWebSocket.Example)
run: >
dotnet publish ObsWebSocket.Example/ObsWebSocket.Example.csproj
--configuration Release
--framework net10.0
--runtime linux-x64
--self-contained true
/p:PublishAot=true
/p:ILLinkTreatWarningsAsErrors=true
/p:IlcTreatWarningsAsErrors=true
/p:WarningsNotAsErrors=IL2104%3BIL3053

- name: Run Unit Tests
# Run tests specifically for the ObsWebSocket.Tests project
# Exclude integration tests which require a live OBS instance
Expand Down
31 changes: 31 additions & 0 deletions ObsWebSocket.Codegen.Tasks/GenerateObsWebSocketSourcesTask.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Microsoft.Build.Framework;

namespace ObsWebSocket.Codegen.Tasks;

public sealed class GenerateObsWebSocketSourcesTask : Microsoft.Build.Utilities.Task
{
[Required]
public string ProtocolPath { get; set; } = string.Empty;

[Required]
public string OutputDirectory { get; set; } = string.Empty;

public bool DownloadIfMissing { get; set; }

public override bool Execute()
{
int exitCode = ProtocolCodegenRunner.GenerateAsync(
protocolPath: ProtocolPath,
outputDirectory: OutputDirectory,
downloadIfMissing: DownloadIfMissing,
cancellationToken: CancellationToken.None,
logInfo: message => Log.LogMessage(MessageImportance.High, message),
logWarning: message => Log.LogWarning(message),
logError: message => Log.LogError(message)
)
.GetAwaiter()
.GetResult();

return exitCode == 0 && !Log.HasLoggedErrors;
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;

namespace ObsWebSocket.SourceGenerators;
namespace ObsWebSocket.Codegen.Tasks.Generation;

/// <summary>
/// Contains DiagnosticDescriptor constants for reporting errors and warnings during source generation.
/// </summary>
[SuppressMessage(
"StyleCop.CSharp.OrderingRules",
"SA1202:Elements must be ordered by access",
Justification = "Constants grouped logically."
)]
internal static class Diagnostics
{
private const string Category = "ObsWebSocketGenerator";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using System.Collections.Concurrent;
using System.Collections.Concurrent;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;

namespace ObsWebSocket.SourceGenerators;
namespace ObsWebSocket.Codegen.Tasks.Generation;

/// <summary>
/// Contains logic for generating Data Transfer Object (DTO) records, including handling nested records.
Expand Down Expand Up @@ -374,6 +374,7 @@ ProtocolDefinition protocol
mainBuilder.AppendLine("using System.Text.Json;");
mainBuilder.AppendLine("using System.Text.Json.Serialization;");
mainBuilder.AppendLine("using System.Diagnostics.CodeAnalysis;");
mainBuilder.AppendLine("using MessagePack;");
mainBuilder.AppendLine($"using {GeneratedCommonNamespace};");
if (!isNestedType)
{
Expand Down Expand Up @@ -445,6 +446,7 @@ ProtocolDefinition protocol
mainBuilder.AppendLine("#pragma warning disable CS8618");

// --- Record Definition Start ---
mainBuilder.AppendLine("[MessagePackObject]");
mainBuilder.AppendLine($"public sealed partial record {recordName}");
mainBuilder.AppendLine("{");
List<PropertyGenInfo> propertyInfos = [];
Expand Down Expand Up @@ -545,6 +547,7 @@ ProtocolDefinition protocol
}

mainBuilder.AppendLine($" [JsonPropertyName(\"{originalName}\")]");
mainBuilder.AppendLine($" [Key(\"{originalName}\")]");
mainBuilder.Append(" public ");
if (isConsideredRequired)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using System.Text;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;

namespace ObsWebSocket.SourceGenerators;
namespace ObsWebSocket.Codegen.Tasks.Generation;

/// <summary>
/// Contains helper methods, constants, and internal data structures for the Emitter class.
Expand Down Expand Up @@ -238,7 +238,7 @@ string parentDtoName
// Map specifically named 'Object' field to Stub record
// Use the fully qualified name to avoid potential namespace conflicts
return ($"{GeneratedCommonNamespace}.SceneItemTransformStub?", false);
// Add other specific 'Object' mappings here if needed in the future
// Add other specific 'Object' mappings here if needed in the future
}
// If not handled above, it falls through to the general 'Object'/'Any' handling below
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis;

namespace ObsWebSocket.SourceGenerators;
namespace ObsWebSocket.Codegen.Tasks.Generation;

/// <summary>
/// Contains logic for building the object hierarchy from dot-notated field definitions.
Expand Down Expand Up @@ -88,7 +88,7 @@ SourceProductionContext context
currentNode = newNode;
}
}
NextFieldPass1:
NextFieldPass1:
;
}

Expand Down
136 changes: 136 additions & 0 deletions ObsWebSocket.Codegen.Tasks/Generation/Emitter.JsonContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;

namespace ObsWebSocket.Codegen.Tasks.Generation;

/// <summary>
/// Contains emitter logic for generating a System.Text.Json source generation context.
/// </summary>
internal static partial class Emitter
{
/// <summary>
/// Generates a JsonSerializerContext containing all fixed and generated protocol types.
/// </summary>
public static void GenerateJsonSerializerContext(
SourceProductionContext context,
ProtocolDefinition protocol
)
{
try
{
StringBuilder builder = BuildSourceHeader("// Serialization Context: ObsWebSocketJsonContext");

_ = builder.AppendLine("using System.Collections.Generic;");
_ = builder.AppendLine("using System.Text.Json;");
_ = builder.AppendLine("using System.Text.Json.Serialization;");
_ = builder.AppendLine("using ObsWebSocket.Core.Protocol;");
_ = builder.AppendLine("using ObsWebSocket.Core.Protocol.Common;");
_ = builder.AppendLine("using ObsWebSocket.Core.Protocol.Events;");
_ = builder.AppendLine("using ObsWebSocket.Core.Protocol.Requests;");
_ = builder.AppendLine("using ObsWebSocket.Core.Protocol.Responses;");
_ = builder.AppendLine();
_ = builder.AppendLine("namespace ObsWebSocket.Core.Serialization;");
_ = builder.AppendLine();
_ = builder.AppendLine("[JsonSourceGenerationOptions(");
_ = builder.AppendLine(" PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,");
_ = builder.AppendLine(" DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault)]");

// Fixed protocol wrapper and payload types.
_ = builder.AppendLine("[JsonSerializable(typeof(OutgoingMessage<RequestPayload>))]");
_ = builder.AppendLine("[JsonSerializable(typeof(OutgoingMessage<IdentifyPayload>))]");
_ = builder.AppendLine("[JsonSerializable(typeof(OutgoingMessage<ReidentifyPayload>))]");
_ = builder.AppendLine("[JsonSerializable(typeof(OutgoingMessage<RequestBatchPayload>))]");
_ = builder.AppendLine("[JsonSerializable(typeof(IncomingMessage<JsonElement>))]");
_ = builder.AppendLine("[JsonSerializable(typeof(RequestResponsePayload<JsonElement>))]");
_ = builder.AppendLine("[JsonSerializable(typeof(RequestBatchResponsePayload<JsonElement>))]");
_ = builder.AppendLine("[JsonSerializable(typeof(EventPayloadBase<JsonElement>))]");
_ = builder.AppendLine("[JsonSerializable(typeof(HelloPayload))]");
_ = builder.AppendLine("[JsonSerializable(typeof(IdentifiedPayload))]");
_ = builder.AppendLine("[JsonSerializable(typeof(RequestStatus))]");

// Handwritten stub types.
_ = builder.AppendLine("[JsonSerializable(typeof(SceneStub))]");
_ = builder.AppendLine("[JsonSerializable(typeof(SceneItemStub))]");
_ = builder.AppendLine("[JsonSerializable(typeof(SceneItemTransformStub))]");
_ = builder.AppendLine("[JsonSerializable(typeof(FilterStub))]");
_ = builder.AppendLine("[JsonSerializable(typeof(InputStub))]");
_ = builder.AppendLine("[JsonSerializable(typeof(TransitionStub))]");
_ = builder.AppendLine("[JsonSerializable(typeof(OutputStub))]");
_ = builder.AppendLine("[JsonSerializable(typeof(MonitorStub))]");
_ = builder.AppendLine("[JsonSerializable(typeof(PropertyItemStub))]");

// Common collection payload helpers.
_ = builder.AppendLine("[JsonSerializable(typeof(List<JsonElement>))]");
_ = builder.AppendLine("[JsonSerializable(typeof(Dictionary<string, bool>))]");
_ = builder.AppendLine("[JsonSerializable(typeof(Dictionary<string, JsonElement>))]");
_ = builder.AppendLine(
"[JsonSerializable(typeof(List<RequestResponsePayload<JsonElement>>))]"
);

// Generated request/response/event DTOs.
if (protocol.Requests is not null)
{
foreach (RequestDefinition request in protocol.Requests)
{
string baseName = SanitizeIdentifier(request.RequestType);
if (request.RequestFields?.Count > 0)
{
_ = builder.AppendLine(
$"[JsonSerializable(typeof({GeneratedRequestsNamespace}.{baseName}RequestData))]"
);
}

if (request.ResponseFields?.Count > 0)
{
_ = builder.AppendLine(
$"[JsonSerializable(typeof({GeneratedResponsesNamespace}.{baseName}ResponseData))]"
);
}
}
}

if (protocol.Events is not null)
{
foreach (OBSEvent eventDef in protocol.Events)
{
if (eventDef.DataFields?.Count > 0)
{
string payloadType = SanitizeIdentifier(eventDef.EventType) + "Payload";
_ = builder.AppendLine(
$"[JsonSerializable(typeof({GeneratedEventsNamespace}.{payloadType}))]"
);
}
}
}

foreach (string nestedTypeName in s_generatedNestedTypes.Keys.OrderBy(k => k))
{
_ = builder.AppendLine(
$"[JsonSerializable(typeof({NestedTypesNamespace}.{nestedTypeName}))]"
);
}

_ = builder.AppendLine("internal sealed partial class ObsWebSocketJsonContext : JsonSerializerContext");
_ = builder.AppendLine("{");
_ = builder.AppendLine("}");

context.AddSource(
"ObsWebSocketJsonContext.g.cs",
SourceText.From(builder.ToString(), Encoding.UTF8)
);
}
catch (Exception ex)
{
context.ReportDiagnostic(
Diagnostic.Create(
Diagnostics.IdentifierGenerationError,
Location.None,
"ObsWebSocketJsonContext",
"JsonSerializerContext generation",
ex.ToString()
)
);
}
}
}
Loading