Skip to content

Commit 63e7155

Browse files
authoredAug 7, 2024··
LSP: Provide output.json option for non-VS Code clients (#972)
And use a reader instead of a string for input as suggested by @charlieegan3 Signed-off-by: Anders Eknert <anders@styra.com>
1 parent 086cb25 commit 63e7155

File tree

4 files changed

+88
-38
lines changed

4 files changed

+88
-38
lines changed
 

‎.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,9 @@ dist/
44

55
/regal
66
/regal.exe
7+
8+
# These two files are used by the Regal evaluation Code Lens, where input.json
9+
# defines the input to use for evaluation, and output.json is where the output
10+
# ends up unless the client supports presenting it in a different way.
11+
input.json
12+
output.json

‎internal/lsp/eval.go

+15-8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"errors"
77
"fmt"
8+
"io"
89
"os"
910
"path"
1011
"path/filepath"
@@ -19,7 +20,7 @@ import (
1920
"github.com/styrainc/regal/pkg/builtins"
2021
)
2122

22-
func (l *LanguageServer) Eval(ctx context.Context, query string, input string) (rego.ResultSet, error) {
23+
func (l *LanguageServer) Eval(ctx context.Context, query string, input io.Reader) (rego.ResultSet, error) {
2324
modules := l.cache.GetAllModules()
2425
moduleFiles := make([]bundle.ModuleFile, 0, len(modules))
2526

@@ -47,10 +48,15 @@ func (l *LanguageServer) Eval(ctx context.Context, query string, input string) (
4748
return nil, fmt.Errorf("failed preparing query: %w", err)
4849
}
4950

50-
if input != "" {
51+
if input != nil {
5152
inputMap := make(map[string]any)
5253

53-
err = json.Unmarshal([]byte(input), &inputMap)
54+
in, err := io.ReadAll(input)
55+
if err != nil {
56+
return nil, fmt.Errorf("failed reading input: %w", err)
57+
}
58+
59+
err = json.Unmarshal(in, &inputMap)
5460
if err != nil {
5561
return nil, fmt.Errorf("failed unmarshalling input: %w", err)
5662
}
@@ -66,22 +72,23 @@ type EvalPathResult struct {
6672
IsUndefined bool `json:"isUndefined"`
6773
}
6874

69-
func FindInput(file string, workspacePath string) string {
75+
func FindInput(file string, workspacePath string) io.Reader {
7076
relative := strings.TrimPrefix(file, workspacePath)
7177
components := strings.Split(path.Dir(relative), string(filepath.Separator))
7278

7379
for i := range len(components) {
7480
inputPath := path.Join(workspacePath, path.Join(components[:len(components)-i]...), "input.json")
7581

76-
if input, err := os.ReadFile(inputPath); err == nil {
77-
return string(input)
82+
f, err := os.Open(inputPath)
83+
if err == nil {
84+
return f
7885
}
7986
}
8087

81-
return ""
88+
return nil
8289
}
8390

84-
func (l *LanguageServer) EvalWorkspacePath(ctx context.Context, query string, input string) (EvalPathResult, error) {
91+
func (l *LanguageServer) EvalWorkspacePath(ctx context.Context, query string, input io.Reader) (EvalPathResult, error) {
8592
resultQuery := "result := " + query
8693

8794
result, err := l.Eval(ctx, resultQuery, input)

‎internal/lsp/eval_test.go

+29-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package lsp
22

33
import (
44
"context"
5+
"io"
56
"os"
7+
"strings"
68
"testing"
79

810
"github.com/styrainc/regal/internal/parse"
@@ -39,7 +41,9 @@ func TestEvalWorkspacePath(t *testing.T) {
3941
ls.cache.SetModule("file://policy1.rego", module1)
4042
ls.cache.SetModule("file://policy2.rego", module2)
4143

42-
res, err := ls.EvalWorkspacePath(context.TODO(), "data.policy1.allow", `{"exists": true}`)
44+
input := strings.NewReader(`{"exists": true}`)
45+
46+
res, err := ls.EvalWorkspacePath(context.TODO(), "data.policy1.allow", input)
4347
if err != nil {
4448
t.Fatal(err)
4549
}
@@ -67,15 +71,15 @@ func TestFindInput(t *testing.T) {
6771
t.Fatal(err)
6872
}
6973

70-
if FindInput(file, workspacePath) != "" {
74+
if readInputString(t, file, workspacePath) != "" {
7175
t.Fatalf("did not expect to find input.json")
7276
}
7377

7478
content := `{"x": 1}`
7579

7680
createWithContent(t, tmpDir+"/workspace/foo/bar/input.json", content)
7781

78-
if res := FindInput(file, workspacePath); res != content {
82+
if res := readInputString(t, file, workspacePath); res != content {
7983
t.Errorf("expected input at %s, got %s", content, res)
8084
}
8185

@@ -86,7 +90,7 @@ func TestFindInput(t *testing.T) {
8690

8791
createWithContent(t, tmpDir+"/workspace/input.json", content)
8892

89-
if res := FindInput(file, workspacePath); res != content {
93+
if res := readInputString(t, file, workspacePath); res != content {
9094
t.Errorf("expected input at %s, got %s", content, res)
9195
}
9296
}
@@ -106,3 +110,24 @@ func createWithContent(t *testing.T, path string, content string) {
106110
t.Fatal(err)
107111
}
108112
}
113+
114+
func readInputString(t *testing.T, file, workspacePath string) string {
115+
t.Helper()
116+
117+
input := FindInput(file, workspacePath)
118+
119+
if input == nil {
120+
return ""
121+
}
122+
123+
bs, err := io.ReadAll(input)
124+
if err != nil {
125+
t.Fatal(err)
126+
}
127+
128+
if bs == nil {
129+
return ""
130+
}
131+
132+
return string(bs)
133+
}

‎internal/lsp/server.go

+38-26
Original file line numberDiff line numberDiff line change
@@ -480,10 +480,8 @@ func (l *LanguageServer) StartCommandWorker(ctx context.Context) {
480480
break
481481
}
482482

483-
input := FindInput(
484-
uri.ToPath(l.clientIdentifier, file),
485-
uri.ToPath(l.clientIdentifier, l.workspaceRootURI),
486-
)
483+
workspacePath := uri.ToPath(l.clientIdentifier, l.workspaceRootURI)
484+
input := FindInput(uri.ToPath(l.clientIdentifier, file), workspacePath)
487485

488486
result, err := l.EvalWorkspacePath(ctx, path, input)
489487
if err != nil {
@@ -492,16 +490,43 @@ func (l *LanguageServer) StartCommandWorker(ctx context.Context) {
492490
break
493491
}
494492

495-
responseParams := map[string]any{
496-
"result": result,
497-
"line": line,
498-
}
493+
if l.clientIdentifier == clients.IdentifierVSCode {
494+
responseParams := map[string]any{
495+
"result": result,
496+
"line": line,
497+
}
499498

500-
responseResult := map[string]any{}
499+
responseResult := map[string]any{}
501500

502-
err = l.conn.Call(ctx, "regal/showEvalResult", responseParams, &responseResult)
503-
if err != nil {
504-
l.logError(fmt.Errorf("failed %s notify: %v", "regal/hello", err.Error()))
501+
err = l.conn.Call(ctx, "regal/showEvalResult", responseParams, &responseResult)
502+
if err != nil {
503+
l.logError(fmt.Errorf("failed %s notify: %v", "regal/hello", err.Error()))
504+
}
505+
} else {
506+
output := filepath.Join(workspacePath, "output.json")
507+
508+
var f *os.File
509+
510+
f, err = os.OpenFile(output, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o755)
511+
512+
if err == nil {
513+
var jsonVal []byte
514+
515+
value := result.Value
516+
if result.IsUndefined {
517+
// Display undefined as an empty object
518+
// we could also go with "<undefined>" or similar
519+
value = make(map[string]any)
520+
}
521+
522+
jsonVal, err = json.MarshalIndent(value, "", " ")
523+
if err == nil {
524+
// staticcheck thinks err here is never used, but I think that's false?
525+
_, err = f.Write(jsonVal) //nolint:staticcheck
526+
}
527+
528+
f.Close()
529+
}
505530
}
506531
}
507532

@@ -1000,13 +1025,6 @@ func (l *LanguageServer) handleTextDocumentCodeLens(
10001025
return nil, fmt.Errorf("failed to unmarshal params: %w", err)
10011026
}
10021027

1003-
if l.clientIdentifier != clients.IdentifierVSCode {
1004-
// only VSCode has the client side capability to handle the callback request
1005-
// to handle the result of evaluation, so displaying code lenses for any other
1006-
// editor is likely just going to result in a bad experience
1007-
return nil, nil // return a null response, as per the spec
1008-
}
1009-
10101028
module, ok := l.cache.GetModule(params.TextDocument.URI)
10111029
if !ok {
10121030
l.logError(fmt.Errorf("failed to get module for uri %q", params.TextDocument.URI))
@@ -1736,16 +1754,10 @@ func (l *LanguageServer) handleInitialize(
17361754
LabelDetailsSupport: true,
17371755
},
17381756
},
1757+
CodeLensProvider: &types.CodeLensOptions{},
17391758
},
17401759
}
17411760

1742-
// Since evaluation requires some client side handling, this can't be supported
1743-
// purely by the LSP. Clients that are capable of handling the code lens callback
1744-
// should be added here though.
1745-
if l.clientIdentifier == clients.IdentifierVSCode {
1746-
initializeResult.Capabilities.CodeLensProvider = &types.CodeLensOptions{}
1747-
}
1748-
17491761
if l.workspaceRootURI != "" {
17501762
configFile, err := config.FindConfig(uri.ToPath(l.clientIdentifier, l.workspaceRootURI))
17511763
if err == nil {

0 commit comments

Comments
 (0)
Please sign in to comment.