feat(m9/T31): Monaco JSON-schema hover/completion on value-entry surface

This commit is contained in:
Joseph Doherty
2026-06-18 13:15:54 -04:00
parent 95b8caf284
commit 68c0f7ac59
4 changed files with 286 additions and 13 deletions
@@ -53,6 +53,12 @@ else
private InboundApiSchema? _rootSchema;
private string? _parsedFor;
// The resolved root schema serialized back to a JSON Schema string (with every
// {"$ref":"lib:Name"} inlined). Fed to the raw-JSON escape-hatch Monaco editor
// so Monaco's built-in JSON language offers schema-driven hover + completion on
// the entered JSON. Computed once per parse alongside the resolved schema.
private string? _resolvedSchemaJson;
// Raw text and per-field parse/validation errors keyed by canonical PATH
// (e.g. "order.id", "tags[0]") so nested inputs each carry their own state.
private readonly Dictionary<string, string> _rawText = new();
@@ -70,6 +76,82 @@ else
_shapes = ScriptParameterNames.ParseShapes(ParameterDefinitions);
_rootSchema = await ParseRootAsync();
_topLevelFields = _rootSchema?.Type == "object" ? _rootSchema.Fields : Array.Empty<InboundApiSchemaField>();
_resolvedSchemaJson = _rootSchema is null ? null : ToJsonSchema(_rootSchema);
}
/// <summary>
/// Serializes a (resolved) <see cref="InboundApiSchema"/> back to a JSON Schema
/// string for the escape-hatch Monaco editor. Any <c>{"$ref":"lib:Name"}</c> the
/// parse already inlined is emitted as its resolved shape (no <c>$ref</c> remains),
/// so Monaco's JSON language sees the full type. The shape-only / unresolved-ref
/// placeholder (Type not in the JSON Schema set) collapses to a permissive
/// <c>{}</c> so the editor still validates well-formed JSON without false errors.
/// </summary>
private static string ToJsonSchema(InboundApiSchema schema)
{
using var stream = new System.IO.MemoryStream();
using (var writer = new System.Text.Json.Utf8JsonWriter(stream))
{
WriteSchema(writer, schema);
}
return System.Text.Encoding.UTF8.GetString(stream.ToArray());
}
private static void WriteSchema(System.Text.Json.Utf8JsonWriter writer, InboundApiSchema schema)
{
switch (schema.Type)
{
case "boolean":
case "integer":
case "number":
case "string":
writer.WriteStartObject();
writer.WriteString("type", schema.Type);
writer.WriteEndObject();
break;
case "object":
writer.WriteStartObject();
writer.WriteString("type", "object");
writer.WritePropertyName("properties");
writer.WriteStartObject();
foreach (var field in schema.Fields)
{
writer.WritePropertyName(field.Name);
WriteSchema(writer, field.Schema);
}
writer.WriteEndObject();
var required = schema.Fields.Where(f => f.Required).Select(f => f.Name).ToList();
if (required.Count > 0)
{
writer.WritePropertyName("required");
writer.WriteStartArray();
foreach (var name in required)
{
writer.WriteStringValue(name);
}
writer.WriteEndArray();
}
writer.WriteEndObject();
break;
case "array":
writer.WriteStartObject();
writer.WriteString("type", "array");
if (schema.Items is not null)
{
writer.WritePropertyName("items");
WriteSchema(writer, schema.Items);
}
writer.WriteEndObject();
break;
default: // unresolved $ref / unknown — permissive (accept any JSON).
writer.WriteStartObject();
writer.WriteEndObject();
break;
}
}
/// <summary>
@@ -165,10 +247,13 @@ else
@RenderArray(schema, path)
break;
default: // unresolved $ref / unknown — raw-JSON escape hatch.
<textarea class="form-control form-control-sm font-monospace" rows="3" id="@id"
placeholder='@($"JSON {DisplayType(schema).ToLowerInvariant()}")'
@oninput="e => SetJson(path, (string?)e.Value)">@AsRaw(path)</textarea>
default: // unresolved $ref / unknown — raw-JSON escape hatch (Monaco).
<div class="param-json-editor" data-path="@path">
<MonacoEditor Language="json" Height="120px" ShowToolbar="false"
JsonSchema="@_resolvedSchemaJson"
Value="@AsRaw(path)"
ValueChanged="@(v => SetJson(path, v))" />
</div>
@RenderError(path)
break;
}