Skip to content

Commit 7c16fa3

Browse files
authored
Verify required properties in generated LSP code (#1433)
1 parent 82c0a79 commit 7c16fa3

File tree

4 files changed

+6117
-63
lines changed

4 files changed

+6117
-63
lines changed

internal/lsp/lsproto/_generate/generate.mjs

Lines changed: 77 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -361,44 +361,95 @@ function generateCode() {
361361
writeLine("// Structures\n");
362362

363363
for (const structure of model.structures) {
364-
write(formatDocumentation(structure.documentation));
364+
/**
365+
* @param {string} name
366+
* @param {boolean} includeDocumentation
367+
*/
368+
function generateStructFields(name, includeDocumentation) {
369+
if (includeDocumentation) {
370+
write(formatDocumentation(structure.documentation));
371+
}
372+
373+
writeLine(`type ${name} struct {`);
365374

366-
writeLine(`type ${structure.name} struct {`); // Embed extended types and mixins
367-
for (const e of structure.extends || []) {
368-
if (e.kind !== "reference") {
369-
throw new Error(`Unexpected extends kind: ${e.kind}`);
375+
// Embed extended types and mixins
376+
for (const e of structure.extends || []) {
377+
if (e.kind !== "reference") {
378+
throw new Error(`Unexpected extends kind: ${e.kind}`);
379+
}
380+
writeLine(`\t${e.name}`);
370381
}
371-
writeLine(`\t${e.name}`);
372-
}
373382

374-
for (const m of structure.mixins || []) {
375-
if (m.kind !== "reference") {
376-
throw new Error(`Unexpected mixin kind: ${m.kind}`);
383+
for (const m of structure.mixins || []) {
384+
if (m.kind !== "reference") {
385+
throw new Error(`Unexpected mixin kind: ${m.kind}`);
386+
}
387+
writeLine(`\t${m.name}`);
377388
}
378-
writeLine(`\t${m.name}`);
379-
}
380389

381-
// Insert a blank line after embeds if there were any
382-
if (
383-
(structure.extends && structure.extends.length > 0) ||
384-
(structure.mixins && structure.mixins.length > 0)
385-
) {
386-
writeLine("");
387-
}
390+
// Insert a blank line after embeds if there were any
391+
if (
392+
(structure.extends && structure.extends.length > 0) ||
393+
(structure.mixins && structure.mixins.length > 0)
394+
) {
395+
writeLine("");
396+
}
397+
398+
// Then properties
399+
for (const prop of structure.properties) {
400+
if (includeDocumentation) {
401+
write(formatDocumentation(prop.documentation));
402+
}
403+
404+
const type = resolveType(prop.type);
405+
const goType = prop.optional || type.needsPointer ? `*${type.name}` : type.name;
388406

389-
// Then properties
390-
for (const prop of structure.properties) {
391-
write(formatDocumentation(prop.documentation));
407+
writeLine(`\t${titleCase(prop.name)} ${goType} \`json:"${prop.name}${prop.optional ? ",omitempty" : ""}"\``);
392408

393-
const type = resolveType(prop.type);
394-
const goType = prop.optional || type.needsPointer ? `*${type.name}` : type.name;
409+
if (includeDocumentation) {
410+
writeLine("");
411+
}
412+
}
395413

396-
writeLine(`\t${titleCase(prop.name)} ${goType} \`json:"${prop.name}${prop.optional ? ",omitempty" : ""}"\``);
414+
writeLine("}");
397415
writeLine("");
398416
}
399417

400-
writeLine("}");
418+
generateStructFields(structure.name, true);
401419
writeLine("");
420+
421+
// Generate UnmarshalJSON method for structure validation
422+
const requiredProps = structure.properties?.filter(p => !p.optional) || [];
423+
if (requiredProps.length > 0) {
424+
writeLine(`func (s *${structure.name}) UnmarshalJSON(data []byte) error {`);
425+
writeLine(`\t// Check required props`);
426+
writeLine(`\ttype requiredProps struct {`);
427+
for (const prop of requiredProps) {
428+
writeLine(`\t\t${titleCase(prop.name)} requiredProp \`json:"${prop.name}"\``);
429+
}
430+
writeLine(`}`);
431+
writeLine("");
432+
433+
writeLine(`\tvar keys requiredProps`);
434+
writeLine(`\tif err := json.Unmarshal(data, &keys); err != nil {`);
435+
writeLine(`\t\treturn err`);
436+
writeLine(`\t}`);
437+
writeLine("");
438+
439+
// writeLine(`\t// Check for missing required keys`);
440+
for (const prop of requiredProps) {
441+
writeLine(`if !keys.${titleCase(prop.name)} {`);
442+
writeLine(`\t\treturn fmt.Errorf("required key '${prop.name}' is missing")`);
443+
writeLine(`}`);
444+
}
445+
446+
writeLine(``);
447+
writeLine(`\t// Redeclare the struct to prevent infinite recursion`);
448+
generateStructFields("temp", false);
449+
writeLine(`\treturn json.Unmarshal(data, (*temp)(s))`);
450+
writeLine(`}`);
451+
writeLine("");
452+
}
402453
}
403454

404455
// Generate enumerations

internal/lsp/lsproto/lsp.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package lsproto
33
import (
44
"encoding/json"
55
"fmt"
6+
7+
"github.com/go-json-experiment/json/jsontext"
68
)
79

810
type DocumentUri string // !!!
@@ -74,3 +76,19 @@ func assertOnlyOne(message string, values ...bool) {
7476
panic(message)
7577
}
7678
}
79+
80+
func ptrTo[T any](v T) *T {
81+
return &v
82+
}
83+
84+
type requiredProp bool
85+
86+
func (v *requiredProp) UnmarshalJSON(data []byte) error {
87+
*v = true
88+
return nil
89+
}
90+
91+
func (v *requiredProp) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
92+
*v = true
93+
return dec.SkipValue()
94+
}

0 commit comments

Comments
 (0)