Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 32 additions & 4 deletions ObsWebSocket.Codegen.Tasks/Generation/Diagnostics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,9 @@ internal static class Diagnostics
public static readonly DiagnosticDescriptor ArrayItemTypeUnknownWarning = new(
id: "OBSWSGEN010",
title: "Array item type unknown",
messageFormat: "Could not determine item type for array field '{0}' in '{1}' from type string '{2}'. Mapping to 'List<System.Text.Json.JsonElement>'.",
messageFormat: "Could not determine item type for array field '{0}' in '{1}' from type string '{2}'. Map it to a stub; List<JsonElement> has no MessagePack formatter in most resolver chains and the message becomes unreadable on that transport.",
category: Category,
defaultSeverity: DiagnosticSeverity.Warning, // Warning as List<JsonElement> is usable
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true
);

Expand All @@ -121,9 +121,37 @@ internal static class Diagnostics
public static readonly DiagnosticDescriptor UnclassifiedNumberField = new(
id: "OBSWSGEN012",
title: "Unclassified Number field",
messageFormat: "Number field '{0}' in '{1}' is not listed in NumericFieldTable. Mapping to 'double'. Add it to the table if it holds whole numbers.",
messageFormat: "Number field '{0}' in '{1}' is not listed in NumericFieldTable. Classify it deliberately: whole numbers reach callers as floating point otherwise.",
category: Category,
defaultSeverity: DiagnosticSeverity.Warning,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true
);

/// <summary>
/// Reported when a field the numeric table calls a whole number carries a protocol restriction
/// written with a decimal point. The restriction is the protocol stating the field is
/// fractional, so the classification is wrong and the value would be truncated on the wire.
/// </summary>
public static readonly DiagnosticDescriptor FractionalFieldClassifiedAsWhole = new(
id: "OBSWSGEN013",
title: "Whole-number field has a fractional restriction",
messageFormat: "Number field '{0}' is listed as a whole number but the protocol restricts it to '{1}', which is fractional. Move it to the double set.",
category: Category,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true
);

/// <summary>
/// Reported when the protocol declares a string-valued enum that no field is mapped onto. The
/// generated property would be a plain string, which is the state every string enum was in
/// before the mapping table existed.
/// </summary>
public static readonly DiagnosticDescriptor UnmappedStringEnum = new(
id: "OBSWSGEN014",
title: "Unmapped string enum",
messageFormat: "The protocol declares string-valued enum '{0}' but no field maps onto it. Add its fields to StringEnumFieldTable, or the properties stay strings.",
category: Category,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true
);

Expand Down
71 changes: 48 additions & 23 deletions ObsWebSocket.Codegen.Tasks/Generation/Emitter.Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@ private static StringBuilder BuildSourceHeader(string? fileTypeComment = null)
/// <param name="field">The field definition being mapped.</param>
/// <param name="parentDtoName">Name of the DTO this field belongs to (for diagnostics).</param>
/// <returns>A tuple containing the C# type name (or null if unmappable) and a boolean indicating if it's a value type.</returns>
[System.Text.RegularExpressions.GeneratedRegex(@"\d\.\d")]
private static partial System.Text.RegularExpressions.Regex FractionalRestriction();

private static (string? CSharpType, bool IsValueType) MapProtocolTypeToCSharp(
SourceProductionContext context,
FieldDefinition field,
Expand All @@ -237,7 +240,11 @@ string parentDtoName
case "sceneItemTransform":
// Map specifically named 'Object' field to Stub record
// Use the fully qualified name to avoid potential namespace conflicts
return ($"{GeneratedCommonNamespace}.SceneItemTransformStub?", false);
// SetSceneItemTransform applies only the fields present, so a request carries
// a patch. A response carries the whole transform OBS computed.
return parentDtoName.EndsWith("RequestData", StringComparison.Ordinal)
? ($"{GeneratedCommonNamespace}.SceneItemTransformPatchStub?", false)
: ($"{GeneratedCommonNamespace}.SceneItemTransformStub?", false);
// 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 All @@ -248,6 +255,27 @@ string parentDtoName
if (obsType == "Number")
{
numberType = NumericFieldTable.MapNumber(fieldName, out bool classified);

// A restriction written with a decimal point is the protocol saying the field is
// fractional. SetTBarPosition shipped as an int this way, so only the two ends of the
// T-bar could be reached.
if (
classified
&& numberType != "double"
&& field.ValueRestrictions is { Length: > 0 } restrictions
&& FractionalRestriction().IsMatch(restrictions)
)
{
context.ReportDiagnostic(
Diagnostic.Create(
Diagnostics.FractionalFieldClassifiedAsWhole,
Location.None,
fieldName,
restrictions
)
);
}

if (!classified)
{
context.ReportDiagnostic(
Expand Down Expand Up @@ -324,14 +352,18 @@ string parentDtoName
// Use fully qualified names for stub types to avoid ambiguity
string? stubType = fieldName switch
{
// The reindex event asks OBS for the basic list, which is id and index only.
"sceneItems" when parentDtoName == "SceneItemListReindexedPayload" =>
$"{GeneratedCommonNamespace}.SceneItemOrderStub",
"sceneItems" => $"{GeneratedCommonNamespace}.SceneItemStub",
"filters" => $"{GeneratedCommonNamespace}.FilterStub",
// Need to check fully qualified parent name to exclude InputVolumeMetersPayload
"inputs"
when parentDtoName
!= $"{GeneratedEventsNamespace}.InputVolumeMetersPayload" =>
$"{GeneratedCommonNamespace}.InputStub",
// The meter payload carries only name, uuid and levels, so it is not an
// InputStub. parentDtoName arrives unqualified.
"inputs" when parentDtoName == "InputVolumeMetersPayload" =>
$"{GeneratedCommonNamespace}.InputVolumeMeterStub",
"inputs" => $"{GeneratedCommonNamespace}.InputStub",
"scenes" => $"{GeneratedCommonNamespace}.SceneStub",
"canvases" => $"{GeneratedCommonNamespace}.CanvasStub",
"outputs" => $"{GeneratedCommonNamespace}.OutputStub",
"transitions" => $"{GeneratedCommonNamespace}.TransitionStub",
"monitors" => $"{GeneratedCommonNamespace}.MonitorStub",
Expand All @@ -344,24 +376,17 @@ when parentDtoName
// Use fully qualified List<T>
return ($"System.Collections.Generic.List<{stubType}>?", false); // List of specific stub type
}
else // Fallback for unknown or explicitly excluded Array<Object>
else // Fallback for an array whose item type is not mapped to a stub.
{
// Only warn if it's truly unknown, not the handled InputVolumeMeters case
if (
fieldName != "inputs"
|| parentDtoName != $"{GeneratedEventsNamespace}.InputVolumeMetersPayload"
)
{
context.ReportDiagnostic(
Diagnostic.Create(
Diagnostics.ArrayItemTypeUnknownWarning,
Location.None,
fieldName,
parentDtoName,
obsType
)
);
}
context.ReportDiagnostic(
Diagnostic.Create(
Diagnostics.ArrayItemTypeUnknownWarning,
Location.None,
fieldName,
parentDtoName,
obsType
)
);
// Fallback to List<JsonElement> for InputVolumeMetersPayload.inputs and any other unmapped Array<Object>
return (
"System.Collections.Generic.List<System.Text.Json.JsonElement>?",
Expand Down
6 changes: 6 additions & 0 deletions ObsWebSocket.Codegen.Tasks/Generation/Emitter.JsonContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,18 @@ ProtocolDefinition protocol
_ = builder.AppendLine("[JsonSerializable(typeof(SceneStub))]");
_ = builder.AppendLine("[JsonSerializable(typeof(SceneItemStub))]");
_ = builder.AppendLine("[JsonSerializable(typeof(SceneItemTransformStub))]");
_ = builder.AppendLine("[JsonSerializable(typeof(SceneItemTransformPatchStub))]");
_ = 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))]");
_ = builder.AppendLine("[JsonSerializable(typeof(InputVolumeMeterStub))]");
_ = builder.AppendLine("[JsonSerializable(typeof(SceneItemOrderStub))]");
_ = builder.AppendLine("[JsonSerializable(typeof(CanvasStub))]");
_ = builder.AppendLine("[JsonSerializable(typeof(CanvasFlagsStub))]");
_ = builder.AppendLine("[JsonSerializable(typeof(CanvasVideoSettingsStub))]");

// Common collection payload helpers.
_ = builder.AppendLine("[JsonSerializable(typeof(List<JsonElement>))]");
Expand Down
3 changes: 2 additions & 1 deletion ObsWebSocket.Codegen.Tasks/Generation/NumericFieldTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ internal static class NumericFieldTable
"sceneItemIndex",
"filterIndex",
"monitorIndex",
"position",
"searchOffset",
// Resolutions, in pixels.
"baseWidth",
Expand Down Expand Up @@ -75,6 +74,8 @@ internal static class NumericFieldTable
"inputVolumeMul",
"inputVolumeDb",
"inputAudioBalance",
// The T-bar, 0.0 to 1.0. As an int only the two ends were reachable.
"position",
"transitionCursor",
"outputCongestion",
"cpuUsage",
Expand Down
49 changes: 49 additions & 0 deletions ObsWebSocket.Codegen.Tasks/Generation/ProtocolCodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ IReadOnlyList<Diagnostic> Diagnostics
return (context.Sources, context.Diagnostics);
}

ReportUnmappedStringEnums(context, protocol);
Emitter.PreGenerateNestedDtos(context, protocol);
Emitter.GenerateEnums(context, protocol);
Emitter.GenerateRequestDtos(context, protocol);
Expand All @@ -53,4 +54,52 @@ IReadOnlyList<Diagnostic> Diagnostics

return (context.Sources, context.Diagnostics);
}

/// <summary>
/// Fails the build when the protocol declares a string-valued enum that no field is mapped
/// onto.
/// </summary>
/// <remarks>
/// The definition types these fields as plain <c>String</c> and never says which enum they
/// draw from, so the association is hand written. That table cannot be derived, but it can be
/// checked: a protocol refresh introducing a new string enum has to be noticed, or every field
/// carrying it silently stays a string.
/// </remarks>
private static void ReportUnmappedStringEnums(
SourceProductionContext context,
ProtocolDefinition protocol
)
{
if (protocol.Enums is null)
{
return;
}

HashSet<string> mapped = new(StringEnumFieldTable.MappedEnums, StringComparer.Ordinal);

foreach (EnumDefinition definition in protocol.Enums)
{
bool stringValued =
definition.EnumIdentifiers.Count > 0
&& definition.EnumIdentifiers.TrueForAll(i =>
i.EnumValue.ValueKind == System.Text.Json.JsonValueKind.String
);

// The generated C# name drops the protocol's Obs prefix.
string generatedName = definition.EnumType.StartsWith("Obs", StringComparison.Ordinal)
? definition.EnumType["Obs".Length..]
: definition.EnumType;

if (stringValued && !mapped.Contains(generatedName))
{
context.ReportDiagnostic(
Diagnostic.Create(
Diagnostics.UnmappedStringEnum,
Location.None,
definition.EnumType
)
);
}
}
}
}
3 changes: 3 additions & 0 deletions ObsWebSocket.Codegen.Tasks/Generation/StringEnumFieldTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ internal static class StringEnumFieldTable
["mediaAction"] = "MediaInputAction",
};

/// <summary>The enum type names this table maps fields onto.</summary>
public static IEnumerable<string> MappedEnums => s_fieldToEnum.Values;

/// <summary>
/// Returns the enum type name a <c>String</c> field maps to, or <see langword="null"/> when it
/// is an ordinary string.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public sealed partial record InputVolumeMetersPayload
/// </summary>
[JsonPropertyName("inputs")]
[Key("inputs")]
public required System.Collections.Generic.List<ObsWebSocket.Core.Protocol.Common.InputStub> Inputs { get; init; }
public required System.Collections.Generic.List<ObsWebSocket.Core.Protocol.Common.InputVolumeMeterStub> Inputs { get; init; }

/// <summary>Initializes a new instance for deserialization via <see cref="JsonConstructorAttribute"/>.</summary>
[JsonConstructor]
Expand All @@ -40,7 +40,7 @@ public InputVolumeMetersPayload() { }
/// <para>Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible.</para>
/// </summary>
[System.Diagnostics.CodeAnalysis.SetsRequiredMembers]
public InputVolumeMetersPayload(System.Collections.Generic.List<ObsWebSocket.Core.Protocol.Common.InputStub> inputs)
public InputVolumeMetersPayload(System.Collections.Generic.List<ObsWebSocket.Core.Protocol.Common.InputVolumeMeterStub> inputs)
{
this.Inputs = inputs;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public sealed partial record SceneItemListReindexedPayload
/// </summary>
[JsonPropertyName("sceneItems")]
[Key("sceneItems")]
public required System.Collections.Generic.List<ObsWebSocket.Core.Protocol.Common.SceneItemStub> SceneItems { get; init; }
public required System.Collections.Generic.List<ObsWebSocket.Core.Protocol.Common.SceneItemOrderStub> SceneItems { get; init; }

/// <summary>
/// Name of the scene
Expand All @@ -54,7 +54,7 @@ public SceneItemListReindexedPayload() { }
/// <para>Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible.</para>
/// </summary>
[System.Diagnostics.CodeAnalysis.SetsRequiredMembers]
public SceneItemListReindexedPayload(string sceneName, string sceneUuid, System.Collections.Generic.List<ObsWebSocket.Core.Protocol.Common.SceneItemStub> sceneItems)
public SceneItemListReindexedPayload(string sceneName, string sceneUuid, System.Collections.Generic.List<ObsWebSocket.Core.Protocol.Common.SceneItemOrderStub> sceneItems)
{
this.SceneName = sceneName;
this.SceneUuid = sceneUuid;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public sealed partial record SetSceneItemTransformRequestData
/// </remarks>
[JsonPropertyName("sceneItemTransform")]
[Key("sceneItemTransform")]
public required ObsWebSocket.Core.Protocol.Common.SceneItemTransformStub? SceneItemTransform { get; init; }
public required ObsWebSocket.Core.Protocol.Common.SceneItemTransformPatchStub? SceneItemTransform { get; init; }

/// <summary>
/// Name of the scene the item is in
Expand Down Expand Up @@ -87,7 +87,7 @@ public SetSceneItemTransformRequestData() { }
/// <para>Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible.</para>
/// </summary>
[System.Diagnostics.CodeAnalysis.SetsRequiredMembers]
public SetSceneItemTransformRequestData(int sceneItemId, ObsWebSocket.Core.Protocol.Common.SceneItemTransformStub? sceneItemTransform, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null)
public SetSceneItemTransformRequestData(int sceneItemId, ObsWebSocket.Core.Protocol.Common.SceneItemTransformPatchStub? sceneItemTransform, string? canvasUuid = null, string? sceneName = null, string? sceneUuid = null)
{
this.CanvasUuid = canvasUuid;
this.SceneName = sceneName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public sealed partial record SetTBarPositionRequestData
/// </remarks>
[JsonPropertyName("position")]
[Key("position")]
public required int Position { get; init; }
public required double Position { get; init; }

/// <summary>
/// Whether to release the TBar. Only set `false` if you know that you will be sending another position update
Expand All @@ -57,7 +57,7 @@ public SetTBarPositionRequestData() { }
/// <para>Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible.</para>
/// </summary>
[System.Diagnostics.CodeAnalysis.SetsRequiredMembers]
public SetTBarPositionRequestData(int position, bool? release = null)
public SetTBarPositionRequestData(double position, bool? release = null)
{
this.Position = position;
this.Release = release;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public sealed partial record GetCanvasListResponseData
/// </summary>
[JsonPropertyName("canvases")]
[Key("canvases")]
public required System.Collections.Generic.List<System.Text.Json.JsonElement> Canvases { get; init; }
public required System.Collections.Generic.List<ObsWebSocket.Core.Protocol.Common.CanvasStub> Canvases { get; init; }

/// <summary>Initializes a new instance for deserialization via <see cref="JsonConstructorAttribute"/>.</summary>
[JsonConstructor]
Expand All @@ -40,7 +40,7 @@ public GetCanvasListResponseData() { }
/// <para>Parameters are ordered with required properties first, then optional properties (with defaults). Follows protocol definition order where possible.</para>
/// </summary>
[System.Diagnostics.CodeAnalysis.SetsRequiredMembers]
public GetCanvasListResponseData(System.Collections.Generic.List<System.Text.Json.JsonElement> canvases)
public GetCanvasListResponseData(System.Collections.Generic.List<ObsWebSocket.Core.Protocol.Common.CanvasStub> canvases)
{
this.Canvases = canvases;
}
Expand Down
Loading
Loading