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
@@ -28,6 +28,16 @@
[Parameter] public bool ReadOnly { get; set; } = false;
[Parameter] public bool ShowToolbar { get; set; } = true;
/// <summary>
/// Optional JSON Schema (the resolved schema text — any
/// <c>{"$ref":"lib:Name"}</c> already inlined) applied to this editor's
/// model when <see cref="Language"/> is <c>json</c>. Wires Monaco's
/// built-in JSON language so the editor offers schema-driven hover,
/// completion, and diagnostics on the entered JSON. Ignored for non-JSON
/// languages. Updating it re-applies the schema to the live model.
/// </summary>
[Parameter] public string? JsonSchema { get; set; }
/// <summary>
/// Runtime globals surface the script is analyzed against. Defaults to
/// template/shared-script globals; set to <c>InboundApi</c> on the API
@@ -87,6 +97,7 @@
private DotNetObjectReference<MonacoEditor>? _dotNetRef;
private readonly string _id = Guid.NewGuid().ToString("N");
private string _lastSentValue = "";
private string? _lastSentSchema;
private bool _initialized;
private bool _wrap;
private bool _minimap;
@@ -108,10 +119,12 @@
{
value = Value ?? "",
language = Language,
readOnly = ReadOnly
readOnly = ReadOnly,
jsonSchema = JsonSchema
},
_dotNetRef);
_initialized = true;
_lastSentSchema = JsonSchema;
}
catch (InvalidOperationException)
{
@@ -128,10 +141,18 @@
Logger.LogError(ex, "Monaco editor {EditorId} failed to initialize.", _id);
}
}
else if (_initialized && (Value ?? "") != _lastSentValue)
else if (_initialized)
{
_lastSentValue = Value ?? "";
await SafeInvokeAsync("MonacoBlazor.setValue", "set editor value", _id, _lastSentValue);
if ((Value ?? "") != _lastSentValue)
{
_lastSentValue = Value ?? "";
await SafeInvokeAsync("MonacoBlazor.setValue", "set editor value", _id, _lastSentValue);
}
if (JsonSchema != _lastSentSchema)
{
_lastSentSchema = JsonSchema;
await SafeInvokeAsync("MonacoBlazor.setJsonSchema", "set JSON schema", _id, JsonSchema);
}
}
}
@@ -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;
}