From 0330d34bb1756ae9e4144494339d5a96b030489b Mon Sep 17 00:00:00 2001 From: Heng Lu Date: Mon, 15 Apr 2024 15:57:37 +0800 Subject: [PATCH] `azapi_*` - support `payload` --- CHANGELOG.md | 4 + internal/langserver/handlers/complete_test.go | 205 ++++++++++++++++++ internal/langserver/handlers/hover/hover.go | 93 ++++---- internal/langserver/handlers/hover_test.go | 82 +++++++ .../TestCompletion_codeSample/expect.json | 44 +++- .../expect.json | 69 ++++++ .../TestCompletion_payload_actionBody/main.tf | 7 + .../expect.json | 159 ++++++++++++++ .../main.tf | 14 ++ .../TestCompletion_payload_prop/expect.json | 39 ++++ .../TestCompletion_payload_prop/main.tf | 17 ++ .../expect.json | 39 ++++ .../main.tf | 14 ++ .../TestCompletion_payload_value/expect.json | 84 +++++++ .../TestCompletion_payload_value/main.tf | 10 + .../TestHover_payload_prop/expect.json | 20 ++ .../testdata/TestHover_payload_prop/main.tf | 17 ++ .../TestHover_payload_propInArray/expect.json | 20 ++ .../TestHover_payload_propInArray/main.tf | 25 +++ .../main.tf | 18 ++ .../main.tf | 19 ++ .../main.tf | 12 + .../handlers/tfschema/body_candidates.go | 46 +++- .../handlers/tfschema/candidates.go | 18 ++ internal/langserver/handlers/tfschema/init.go | 40 ++++ .../langserver/handlers/validate/validate.go | 41 ++-- .../handlers/validate/validate_test.go | 44 ++++ 27 files changed, 1117 insertions(+), 83 deletions(-) create mode 100644 internal/langserver/handlers/testdata/TestCompletion_payload_actionBody/expect.json create mode 100644 internal/langserver/handlers/testdata/TestCompletion_payload_actionBody/main.tf create mode 100644 internal/langserver/handlers/testdata/TestCompletion_payload_discriminated/expect.json create mode 100644 internal/langserver/handlers/testdata/TestCompletion_payload_discriminated/main.tf create mode 100644 internal/langserver/handlers/testdata/TestCompletion_payload_prop/expect.json create mode 100644 internal/langserver/handlers/testdata/TestCompletion_payload_prop/main.tf create mode 100644 internal/langserver/handlers/testdata/TestCompletion_payload_propWrappedByQuote/expect.json create mode 100644 internal/langserver/handlers/testdata/TestCompletion_payload_propWrappedByQuote/main.tf create mode 100644 internal/langserver/handlers/testdata/TestCompletion_payload_value/expect.json create mode 100644 internal/langserver/handlers/testdata/TestCompletion_payload_value/main.tf create mode 100644 internal/langserver/handlers/testdata/TestHover_payload_prop/expect.json create mode 100644 internal/langserver/handlers/testdata/TestHover_payload_prop/main.tf create mode 100644 internal/langserver/handlers/testdata/TestHover_payload_propInArray/expect.json create mode 100644 internal/langserver/handlers/testdata/TestHover_payload_propInArray/main.tf create mode 100644 internal/langserver/handlers/testdata/TestValidation_payload_missingRequiredProperty/main.tf create mode 100644 internal/langserver/handlers/testdata/TestValidation_payload_missingRequiredPropertyInArrayItem/main.tf create mode 100644 internal/langserver/handlers/testdata/TestValidation_payload_notExpectedProperty/main.tf diff --git a/CHANGELOG.md b/CHANGELOG.md index 78079a237..9691b4585 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ ## v1.13.0 (unreleased) ENHANCEMENTS: - Support the new bicep types. +- `azapi_resource` resource: Support for the `payload` property. +- `azapi_update_resource` resource: Support for the `payload` property. +- `azapi_resource_action` resource: Support for the `payload` property. +- `azapi_resource_action` data source: Support for the `payload` property. ## v1.12.0 ENHANCEMENTS: diff --git a/internal/langserver/handlers/complete_test.go b/internal/langserver/handlers/complete_test.go index 4a2bd9c12..563d26812 100644 --- a/internal/langserver/handlers/complete_test.go +++ b/internal/langserver/handlers/complete_test.go @@ -153,6 +153,47 @@ func TestCompletion_value(t *testing.T) { }, string(expectRaw)) } +func TestCompletion_payload_value(t *testing.T) { + tmpDir := TempDir(t) + InitPluginCache(t, tmpDir.Dir()) + + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{})) + stop := ls.Start(t) + defer stop() + + config, err := os.ReadFile(fmt.Sprintf("./testdata/%s/main.tf", t.Name())) + if err != nil { + t.Fatal(err) + } + + expectRaw, err := os.ReadFile(fmt.Sprintf("./testdata/%s/expect.json", t.Name())) + if err != nil { + t.Fatal(err) + } + + ls.Call(t, &langserver.CallRequest{ + Method: "initialize", + ReqParams: fmt.Sprintf(`{ + "capabilities": {}, + "rootUri": %q, + "processId": 12345 + }`, tmpDir.URI()), + }) + ls.Notify(t, &langserver.CallRequest{ + Method: "initialized", + ReqParams: "{}", + }) + ls.Call(t, &langserver.CallRequest{ + Method: "textDocument/didOpen", + ReqParams: buildReqParamsTextDocument(string(config), tmpDir.URI()), + }) + + ls.CallAndExpectResponse(t, &langserver.CallRequest{ + Method: "textDocument/completion", + ReqParams: buildReqParamsCompletion(7, 15, tmpDir.URI()), + }, string(expectRaw)) +} + func TestCompletion_propWrappedByQuote(t *testing.T) { tmpDir := TempDir(t) InitPluginCache(t, tmpDir.Dir()) @@ -194,6 +235,47 @@ func TestCompletion_propWrappedByQuote(t *testing.T) { }, string(expectRaw)) } +func TestCompletion_payload_propWrappedByQuote(t *testing.T) { + tmpDir := TempDir(t) + InitPluginCache(t, tmpDir.Dir()) + + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{})) + stop := ls.Start(t) + defer stop() + + config, err := os.ReadFile(fmt.Sprintf("./testdata/%s/main.tf", t.Name())) + if err != nil { + t.Fatal(err) + } + + expectRaw, err := os.ReadFile(fmt.Sprintf("./testdata/%s/expect.json", t.Name())) + if err != nil { + t.Fatal(err) + } + + ls.Call(t, &langserver.CallRequest{ + Method: "initialize", + ReqParams: fmt.Sprintf(`{ + "capabilities": {}, + "rootUri": %q, + "processId": 12345 + }`, tmpDir.URI()), + }) + ls.Notify(t, &langserver.CallRequest{ + Method: "initialized", + ReqParams: "{}", + }) + ls.Call(t, &langserver.CallRequest{ + Method: "textDocument/didOpen", + ReqParams: buildReqParamsTextDocument(string(config), tmpDir.URI()), + }) + + ls.CallAndExpectResponse(t, &langserver.CallRequest{ + Method: "textDocument/completion", + ReqParams: buildReqParamsCompletion(9, 11, tmpDir.URI()), + }, string(expectRaw)) +} + func TestCompletion_action(t *testing.T) { tmpDir := TempDir(t) InitPluginCache(t, tmpDir.Dir()) @@ -276,6 +358,47 @@ func TestCompletion_actionBody(t *testing.T) { }, string(expectRaw)) } +func TestCompletion_payload_actionBody(t *testing.T) { + tmpDir := TempDir(t) + InitPluginCache(t, tmpDir.Dir()) + + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{})) + stop := ls.Start(t) + defer stop() + + config, err := os.ReadFile(fmt.Sprintf("./testdata/%s/main.tf", t.Name())) + if err != nil { + t.Fatal(err) + } + + expectRaw, err := os.ReadFile(fmt.Sprintf("./testdata/%s/expect.json", t.Name())) + if err != nil { + t.Fatal(err) + } + + ls.Call(t, &langserver.CallRequest{ + Method: "initialize", + ReqParams: fmt.Sprintf(`{ + "capabilities": {}, + "rootUri": %q, + "processId": 12345 + }`, tmpDir.URI()), + }) + ls.Notify(t, &langserver.CallRequest{ + Method: "initialized", + ReqParams: "{}", + }) + ls.Call(t, &langserver.CallRequest{ + Method: "textDocument/didOpen", + ReqParams: buildReqParamsTextDocument(string(config), tmpDir.URI()), + }) + + ls.CallAndExpectResponse(t, &langserver.CallRequest{ + Method: "textDocument/completion", + ReqParams: buildReqParamsCompletion(5, 5, tmpDir.URI()), + }, string(expectRaw)) +} + func TestCompletion_discriminated(t *testing.T) { tmpDir := TempDir(t) InitPluginCache(t, tmpDir.Dir()) @@ -317,6 +440,47 @@ func TestCompletion_discriminated(t *testing.T) { }, string(expectRaw)) } +func TestCompletion_payload_discriminated(t *testing.T) { + tmpDir := TempDir(t) + InitPluginCache(t, tmpDir.Dir()) + + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{})) + stop := ls.Start(t) + defer stop() + + config, err := os.ReadFile(fmt.Sprintf("./testdata/%s/main.tf", t.Name())) + if err != nil { + t.Fatal(err) + } + + expectRaw, err := os.ReadFile(fmt.Sprintf("./testdata/%s/expect.json", t.Name())) + if err != nil { + t.Fatal(err) + } + + ls.Call(t, &langserver.CallRequest{ + Method: "initialize", + ReqParams: fmt.Sprintf(`{ + "capabilities": {}, + "rootUri": %q, + "processId": 12345 + }`, tmpDir.URI()), + }) + ls.Notify(t, &langserver.CallRequest{ + Method: "initialized", + ReqParams: "{}", + }) + ls.Call(t, &langserver.CallRequest{ + Method: "textDocument/didOpen", + ReqParams: buildReqParamsTextDocument(string(config), tmpDir.URI()), + }) + + ls.CallAndExpectResponse(t, &langserver.CallRequest{ + Method: "textDocument/completion", + ReqParams: buildReqParamsCompletion(11, 7, tmpDir.URI()), + }, string(expectRaw)) +} + func TestCompletion_prop(t *testing.T) { tmpDir := TempDir(t) InitPluginCache(t, tmpDir.Dir()) @@ -358,6 +522,47 @@ func TestCompletion_prop(t *testing.T) { }, string(expectRaw)) } +func TestCompletion_payload_prop(t *testing.T) { + tmpDir := TempDir(t) + InitPluginCache(t, tmpDir.Dir()) + + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{})) + stop := ls.Start(t) + defer stop() + + config, err := os.ReadFile(fmt.Sprintf("./testdata/%s/main.tf", t.Name())) + if err != nil { + t.Fatal(err) + } + + expectRaw, err := os.ReadFile(fmt.Sprintf("./testdata/%s/expect.json", t.Name())) + if err != nil { + t.Fatal(err) + } + + ls.Call(t, &langserver.CallRequest{ + Method: "initialize", + ReqParams: fmt.Sprintf(`{ + "capabilities": {}, + "rootUri": %q, + "processId": 12345 + }`, tmpDir.URI()), + }) + ls.Notify(t, &langserver.CallRequest{ + Method: "initialized", + ReqParams: "{}", + }) + ls.Call(t, &langserver.CallRequest{ + Method: "textDocument/didOpen", + ReqParams: buildReqParamsTextDocument(string(config), tmpDir.URI()), + }) + + ls.CallAndExpectResponse(t, &langserver.CallRequest{ + Method: "textDocument/completion", + ReqParams: buildReqParamsCompletion(12, 11, tmpDir.URI()), + }, string(expectRaw)) +} + func TestCompletion_codeSample(t *testing.T) { tmpDir := TempDir(t) InitPluginCache(t, tmpDir.Dir()) diff --git a/internal/langserver/handlers/hover/hover.go b/internal/langserver/handlers/hover/hover.go index e590bc394..16831ced8 100644 --- a/internal/langserver/handlers/hover/hover.go +++ b/internal/langserver/handlers/hover/hover.go @@ -47,64 +47,39 @@ func HoverAtPos(data []byte, filename string, pos hcl.Pos, logger *log.Logger) * } } } - case "body": + case "payload": if parser.ContainsPos(attribute.NameRange, pos) { return Hover(property.Name, property.Modifier, property.Type, property.Description, attribute.NameRange) } - typeValue := parser.ExtractAzureResourceType(block) - if typeValue == nil { + + bodyDef := tfschema.BodyDefinitionFromBlock(block) + if bodyDef == nil { + return nil + } + + tokens, _ := hclsyntax.LexExpression(data[attribute.Expr.Range().Start.Byte:attribute.Expr.Range().End.Byte], filename, attribute.Expr.Range().Start) + hclNode := parser.BuildHclNode(tokens) + if hclNode == nil { break } - var bodyDef types.TypeBase - def, err := azure.GetResourceDefinitionByResourceType(*typeValue) - if err != nil || def == nil { - return nil + return hoverOnBody(hclNode, pos, bodyDef) + case "body": + if parser.ContainsPos(attribute.NameRange, pos) { + return Hover(property.Name, property.Modifier, property.Type, property.Description, attribute.NameRange) } - bodyDef = def - if len(block.Labels) >= 2 && block.Labels[0] == "azapi_resource_action" { - parts := strings.Split(*typeValue, "@") - if len(parts) != 2 { - return nil - } - actionName := parser.ExtractAction(block) - if actionName != nil && len(*actionName) != 0 { - resourceFuncDef, err := azure.GetResourceFunction(parts[0], parts[1], *actionName) - if err != nil || resourceFuncDef == nil { - return nil - } - bodyDef = resourceFuncDef - } + + bodyDef := tfschema.BodyDefinitionFromBlock(block) + if bodyDef == nil { + return nil } hclNode := parser.JsonEncodeExpressionToHclNode(data, attribute.Expr) if hclNode == nil { break } - hclNodes := parser.HclNodeArraysOfPos(hclNode, pos) - if len(hclNodes) == 0 { - break - } - lastHclNode := hclNodes[len(hclNodes)-1] - if parser.ContainsPos(lastHclNode.KeyRange, pos) { - defs := schema.GetDef(bodyDef.AsTypeBase(), hclNodes[0:len(hclNodes)-1], 0) - props := make([]schema.Property, 0) - for _, def := range defs { - props = append(props, schema.GetAllowedProperties(def)...) - } - logger.Printf("received allowed keys: %#v", props) - if len(props) != 0 { - index := 0 - for i := range props { - if props[i].Name == lastHclNode.Key { - index = i - break - } - } - return Hover(props[index].Name, string(props[index].Modifier), props[index].Type, props[index].Description, lastHclNode.KeyRange) - } - } + return hoverOnBody(hclNode, pos, bodyDef) default: if !parser.ContainsPos(attribute.NameRange, pos) { return nil @@ -132,6 +107,36 @@ func HoverAtPos(data []byte, filename string, pos hcl.Pos, logger *log.Logger) * return nil } +func hoverOnBody(hclNode *parser.HclNode, pos hcl.Pos, bodyDef types.TypeBase) *lsp.Hover { + hclNodes := parser.HclNodeArraysOfPos(hclNode, pos) + if len(hclNodes) == 0 { + return nil + } + lastHclNode := hclNodes[len(hclNodes)-1] + + if parser.ContainsPos(lastHclNode.KeyRange, pos) { + defs := schema.GetDef(bodyDef.AsTypeBase(), hclNodes[0:len(hclNodes)-1], 0) + props := make([]schema.Property, 0) + for _, def := range defs { + props = append(props, schema.GetAllowedProperties(def)...) + } + if len(props) != 0 { + index := -1 + for i := range props { + if props[i].Name == lastHclNode.Key { + index = i + break + } + } + if index == -1 { + return nil + } + return Hover(props[index].Name, string(props[index].Modifier), props[index].Type, props[index].Description, lastHclNode.KeyRange) + } + } + return nil +} + func GetParentType(resourceType string) string { parts := strings.Split(resourceType, "/") if len(parts) <= 2 { diff --git a/internal/langserver/handlers/hover_test.go b/internal/langserver/handlers/hover_test.go index a8143e650..8f3ca756b 100644 --- a/internal/langserver/handlers/hover_test.go +++ b/internal/langserver/handlers/hover_test.go @@ -70,6 +70,47 @@ func TestHover_prop(t *testing.T) { }, string(expectRaw)) } +func TestHover_payload_prop(t *testing.T) { + tmpDir := TempDir(t) + InitPluginCache(t, tmpDir.Dir()) + + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{})) + stop := ls.Start(t) + defer stop() + + config, err := os.ReadFile(fmt.Sprintf("./testdata/%s/main.tf", t.Name())) + if err != nil { + t.Fatal(err) + } + + expectRaw, err := os.ReadFile(fmt.Sprintf("./testdata/%s/expect.json", t.Name())) + if err != nil { + t.Fatal(err) + } + + ls.Call(t, &langserver.CallRequest{ + Method: "initialize", + ReqParams: fmt.Sprintf(`{ + "capabilities": {}, + "rootUri": %q, + "processId": 12345 + }`, TempDir(t).URI()), + }) + ls.Notify(t, &langserver.CallRequest{ + Method: "initialized", + ReqParams: "{}", + }) + ls.Call(t, &langserver.CallRequest{ + Method: "textDocument/didOpen", + ReqParams: buildReqParamsTextDocument(string(config), tmpDir.URI()), + }) + + ls.CallAndExpectResponse(t, &langserver.CallRequest{ + Method: "textDocument/hover", + ReqParams: buildReqParamsHover(11, 14, tmpDir.URI()), + }, string(expectRaw)) +} + func TestHover_propInArray(t *testing.T) { tmpDir := TempDir(t) InitPluginCache(t, tmpDir.Dir()) @@ -111,6 +152,47 @@ func TestHover_propInArray(t *testing.T) { }, string(expectRaw)) } +func TestHover_payload_propInArray(t *testing.T) { + tmpDir := TempDir(t) + InitPluginCache(t, tmpDir.Dir()) + + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{})) + stop := ls.Start(t) + defer stop() + + config, err := os.ReadFile(fmt.Sprintf("./testdata/%s/main.tf", t.Name())) + if err != nil { + t.Fatal(err) + } + + expectRaw, err := os.ReadFile(fmt.Sprintf("./testdata/%s/expect.json", t.Name())) + if err != nil { + t.Fatal(err) + } + + ls.Call(t, &langserver.CallRequest{ + Method: "initialize", + ReqParams: fmt.Sprintf(`{ + "capabilities": {}, + "rootUri": %q, + "processId": 12345 + }`, TempDir(t).URI()), + }) + ls.Notify(t, &langserver.CallRequest{ + Method: "initialized", + ReqParams: "{}", + }) + ls.Call(t, &langserver.CallRequest{ + Method: "textDocument/didOpen", + ReqParams: buildReqParamsTextDocument(string(config), tmpDir.URI()), + }) + + ls.CallAndExpectResponse(t, &langserver.CallRequest{ + Method: "textDocument/hover", + ReqParams: buildReqParamsHover(13, 13, tmpDir.URI()), + }, string(expectRaw)) +} + func buildReqParamsHover(line int, character int, uri string) string { param := make(map[string]interface{}) textDocument := make(map[string]interface{}) diff --git a/internal/langserver/handlers/testdata/TestCompletion_codeSample/expect.json b/internal/langserver/handlers/testdata/TestCompletion_codeSample/expect.json index 636e6c365..1427d6612 100644 --- a/internal/langserver/handlers/testdata/TestCompletion_codeSample/expect.json +++ b/internal/langserver/handlers/testdata/TestCompletion_codeSample/expect.json @@ -210,6 +210,36 @@ "command": "editor.action.triggerSuggest" } }, + { + "label": "payload", + "labelDetails": {}, + "kind": 10, + "detail": "payload (Optional)", + "documentation": { + "kind": "markdown", + "value": "Type: `dynamic` \nA dynamic attribute that contains the request body used to create and update azure resource.\n" + }, + "sortText": "0006", + "insertTextFormat": 2, + "insertTextMode": 2, + "textEdit": { + "range": { + "start": { + "line": 2, + "character": 2 + }, + "end": { + "line": 2, + "character": 2 + } + }, + "newText": "payload = $0" + }, + "command": { + "title": "Suggest", + "command": "editor.action.triggerSuggest" + } + }, { "label": "tags", "labelDetails": {}, @@ -219,7 +249,7 @@ "kind": "markdown", "value": "Type: `map` \nA mapping of tags which should be assigned to the azure resource.\n" }, - "sortText": "0006", + "sortText": "0007", "insertTextFormat": 2, "insertTextMode": 2, "textEdit": { @@ -249,7 +279,7 @@ "kind": "markdown", "value": "Type: `list` \nA list of path that needs to be exported from response body.\n" }, - "sortText": "0007", + "sortText": "0008", "insertTextFormat": 2, "insertTextMode": 2, "textEdit": { @@ -279,7 +309,7 @@ "kind": "markdown", "value": "Type: `bool` \nWhether enabled the validation on `type` and `body` with embedded schema. Defaults to `true`.\n" }, - "sortText": "0008", + "sortText": "0009", "insertTextFormat": 2, "insertTextMode": 2, "textEdit": { @@ -309,7 +339,7 @@ "kind": "markdown", "value": "Type: `list` \nA list of ARM resource IDs which are used to avoid create/modify/delete azapi resources at the same time.\n" }, - "sortText": "0009", + "sortText": "0010", "insertTextFormat": 2, "insertTextMode": 2, "textEdit": { @@ -339,7 +369,7 @@ "kind": "markdown", "value": "Type: `list` \nA list of properties that should be ignored when comparing the `body` with its current state..\n" }, - "sortText": "0010", + "sortText": "0011", "insertTextFormat": 2, "insertTextMode": 2, "textEdit": { @@ -369,7 +399,7 @@ "kind": "markdown", "value": "Type: `bool` \nWhether ignore incorrect casing returned in `body` to suppress plan-diff. Defaults to `false`.\n" }, - "sortText": "0011", + "sortText": "0012", "insertTextFormat": 2, "insertTextMode": 2, "textEdit": { @@ -399,7 +429,7 @@ "kind": "markdown", "value": "Type: `bool` \nWhether ignore not returned properties like credentials in `body` to suppress plan-diff. Defaults to `false`.\n" }, - "sortText": "0012", + "sortText": "0013", "insertTextFormat": 2, "insertTextMode": 2, "textEdit": { diff --git a/internal/langserver/handlers/testdata/TestCompletion_payload_actionBody/expect.json b/internal/langserver/handlers/testdata/TestCompletion_payload_actionBody/expect.json new file mode 100644 index 000000000..b91aa9aaa --- /dev/null +++ b/internal/langserver/handlers/testdata/TestCompletion_payload_actionBody/expect.json @@ -0,0 +1,69 @@ +{ + "jsonrpc": "2.0", + "id": 3, + "result": { + "isIncomplete": false, + "items": [ + { + "label": "required-properties", + "labelDetails": {}, + "kind": 15, + "detail": "Required properties", + "documentation": { + "kind": "markdown", + "value": "Type: `` \n```\nkeyType = \"$1\"\n\n```\n" + }, + "sortText": "0", + "insertTextFormat": 2, + "insertTextMode": 2, + "textEdit": { + "range": { + "start": { + "line": 4, + "character": 4 + }, + "end": { + "line": 4, + "character": 4 + } + }, + "newText": "keyType = \"$1\"\n" + }, + "command": { + "title": "Suggest", + "command": "editor.action.triggerSuggest" + } + }, + { + "label": "keyType", + "labelDetails": {}, + "kind": 10, + "detail": "keyType (Required)", + "documentation": { + "kind": "markdown", + "value": "Type: `string` \nType of the test key\n" + }, + "sortText": "0keyType", + "insertTextFormat": 2, + "insertTextMode": 2, + "textEdit": { + "range": { + "start": { + "line": 4, + "character": 4 + }, + "end": { + "line": 4, + "character": 4 + } + }, + "newText": "keyType = \"$0\"" + }, + "command": { + "title": "Suggest", + "command": "editor.action.triggerSuggest" + } + } + ] + } +} \ No newline at end of file diff --git a/internal/langserver/handlers/testdata/TestCompletion_payload_actionBody/main.tf b/internal/langserver/handlers/testdata/TestCompletion_payload_actionBody/main.tf new file mode 100644 index 000000000..0554493d4 --- /dev/null +++ b/internal/langserver/handlers/testdata/TestCompletion_payload_actionBody/main.tf @@ -0,0 +1,7 @@ +resource "azapi_resource_action" "test" { + type = "Microsoft.AppPlatform/Spring@2022-05-01-preview" + action = "regenerateTestKey" + payload = { + + } +} \ No newline at end of file diff --git a/internal/langserver/handlers/testdata/TestCompletion_payload_discriminated/expect.json b/internal/langserver/handlers/testdata/TestCompletion_payload_discriminated/expect.json new file mode 100644 index 000000000..bd307727f --- /dev/null +++ b/internal/langserver/handlers/testdata/TestCompletion_payload_discriminated/expect.json @@ -0,0 +1,159 @@ +{ + "jsonrpc": "2.0", + "id": 3, + "result": { + "isIncomplete": false, + "items": [ + { + "label": "type", + "labelDetails": {}, + "kind": 10, + "detail": "type (Required)", + "documentation": { + "kind": "markdown", + "value": "Type: `string` \nType of data flow.\n" + }, + "sortText": "0type", + "insertTextFormat": 2, + "insertTextMode": 2, + "textEdit": { + "range": { + "start": { + "line": 10, + "character": 6 + }, + "end": { + "line": 10, + "character": 6 + } + }, + "newText": "type = \"$0\"" + }, + "command": { + "title": "Suggest", + "command": "editor.action.triggerSuggest" + } + }, + { + "label": "annotations", + "labelDetails": {}, + "kind": 10, + "detail": "annotations (Optional)", + "documentation": { + "kind": "markdown", + "value": "Type: `array` \nList of tags that can be used for describing the data flow.\n" + }, + "sortText": "1annotations", + "insertTextFormat": 2, + "insertTextMode": 2, + "textEdit": { + "range": { + "start": { + "line": 10, + "character": 6 + }, + "end": { + "line": 10, + "character": 6 + } + }, + "newText": "annotations = [$0]" + }, + "command": { + "title": "Suggest", + "command": "editor.action.triggerSuggest" + } + }, + { + "label": "description", + "labelDetails": {}, + "kind": 10, + "detail": "description (Optional)", + "documentation": { + "kind": "markdown", + "value": "Type: `string` \nThe description of the data flow.\n" + }, + "sortText": "1description", + "insertTextFormat": 2, + "insertTextMode": 2, + "textEdit": { + "range": { + "start": { + "line": 10, + "character": 6 + }, + "end": { + "line": 10, + "character": 6 + } + }, + "newText": "description = \"$0\"" + }, + "command": { + "title": "Suggest", + "command": "editor.action.triggerSuggest" + } + }, + { + "label": "folder", + "labelDetails": {}, + "kind": 10, + "detail": "folder (Optional)", + "documentation": { + "kind": "markdown", + "value": "Type: `object` \nThe folder that this data flow is in. If not specified, Data flow will appear at the root level.\n" + }, + "sortText": "1folder", + "insertTextFormat": 2, + "insertTextMode": 2, + "textEdit": { + "range": { + "start": { + "line": 10, + "character": 6 + }, + "end": { + "line": 10, + "character": 6 + } + }, + "newText": "folder = {\n\t$0\n}" + }, + "command": { + "title": "Suggest", + "command": "editor.action.triggerSuggest" + } + }, + { + "label": "typeProperties", + "labelDetails": {}, + "kind": 10, + "detail": "typeProperties (Optional)", + "documentation": { + "kind": "markdown", + "value": "Type: `object` \nFlowlet type properties.\n" + }, + "sortText": "1typeProperties", + "insertTextFormat": 2, + "insertTextMode": 2, + "textEdit": { + "range": { + "start": { + "line": 10, + "character": 6 + }, + "end": { + "line": 10, + "character": 6 + } + }, + "newText": "typeProperties = {\n\t$0\n}" + }, + "command": { + "title": "Suggest", + "command": "editor.action.triggerSuggest" + } + } + ] + } +} \ No newline at end of file diff --git a/internal/langserver/handlers/testdata/TestCompletion_payload_discriminated/main.tf b/internal/langserver/handlers/testdata/TestCompletion_payload_discriminated/main.tf new file mode 100644 index 000000000..e89c072dc --- /dev/null +++ b/internal/langserver/handlers/testdata/TestCompletion_payload_discriminated/main.tf @@ -0,0 +1,14 @@ +resource "azapi_resource" "dataflow" { + type = "Microsoft.DataFactory/factories/dataflows@2018-06-01" + name = "hengludf0509" + parent_id = azurerm_data_factory.test.id + payload = { + properties = { + type = "Flowlet" + typeProperties = { + + } + + } + } +} \ No newline at end of file diff --git a/internal/langserver/handlers/testdata/TestCompletion_payload_prop/expect.json b/internal/langserver/handlers/testdata/TestCompletion_payload_prop/expect.json new file mode 100644 index 000000000..4491b83de --- /dev/null +++ b/internal/langserver/handlers/testdata/TestCompletion_payload_prop/expect.json @@ -0,0 +1,39 @@ +{ + "jsonrpc": "2.0", + "id": 3, + "result": { + "isIncomplete": false, + "items": [ + { + "label": "userAssignedIdentity", + "labelDetails": {}, + "kind": 10, + "detail": "userAssignedIdentity (Optional)", + "documentation": { + "kind": "markdown", + "value": "Type: `string` \nThe resource id of the user assigned identity to authenticate to customer's key vault.\n" + }, + "sortText": "1userAssignedIdentity", + "insertTextFormat": 2, + "insertTextMode": 2, + "textEdit": { + "range": { + "start": { + "line": 11, + "character": 10 + }, + "end": { + "line": 11, + "character": 10 + } + }, + "newText": "userAssignedIdentity = \"$0\"" + }, + "command": { + "title": "Suggest", + "command": "editor.action.triggerSuggest" + } + } + ] + } +} \ No newline at end of file diff --git a/internal/langserver/handlers/testdata/TestCompletion_payload_prop/main.tf b/internal/langserver/handlers/testdata/TestCompletion_payload_prop/main.tf new file mode 100644 index 000000000..46c75dee4 --- /dev/null +++ b/internal/langserver/handlers/testdata/TestCompletion_payload_prop/main.tf @@ -0,0 +1,17 @@ +resource "azapi_resource" "test" { + name = "acctest1774" + parent_id = azurerm_batch_account.test.id + type = "Microsoft.DataFactory/factories@2018-06-01" + payload = { + identity = { + type = "SystemAssigned" + } + properties = { + encryption = { + identity = { + + } + } + } + } +} diff --git a/internal/langserver/handlers/testdata/TestCompletion_payload_propWrappedByQuote/expect.json b/internal/langserver/handlers/testdata/TestCompletion_payload_propWrappedByQuote/expect.json new file mode 100644 index 000000000..7f68957b9 --- /dev/null +++ b/internal/langserver/handlers/testdata/TestCompletion_payload_propWrappedByQuote/expect.json @@ -0,0 +1,39 @@ +{ + "jsonrpc": "2.0", + "id": 3, + "result": { + "isIncomplete": false, + "items": [ + { + "label": "userAssignedIdentity", + "labelDetails": {}, + "kind": 10, + "detail": "userAssignedIdentity (Optional)", + "documentation": { + "kind": "markdown", + "value": "Type: `string` \nThe resource id of the user assigned identity to authenticate to customer's key vault.\n" + }, + "sortText": "1userAssignedIdentity", + "insertTextFormat": 2, + "insertTextMode": 2, + "textEdit": { + "range": { + "start": { + "line": 8, + "character": 10 + }, + "end": { + "line": 8, + "character": 10 + } + }, + "newText": "\"userAssignedIdentity\": \"$0\"" + }, + "command": { + "title": "Suggest", + "command": "editor.action.triggerSuggest" + } + } + ] + } +} \ No newline at end of file diff --git a/internal/langserver/handlers/testdata/TestCompletion_payload_propWrappedByQuote/main.tf b/internal/langserver/handlers/testdata/TestCompletion_payload_propWrappedByQuote/main.tf new file mode 100644 index 000000000..8428f9435 --- /dev/null +++ b/internal/langserver/handlers/testdata/TestCompletion_payload_propWrappedByQuote/main.tf @@ -0,0 +1,14 @@ +resource "azapi_resource" "test" { + name = "acctest1774" + parent_id = azurerm_batch_account.test.id + type = "Microsoft.DataFactory/factories@2018-06-01" + payload = { + "properties": { + "encryption": { + "identity": { + + } + } + } + } +} diff --git a/internal/langserver/handlers/testdata/TestCompletion_payload_value/expect.json b/internal/langserver/handlers/testdata/TestCompletion_payload_value/expect.json new file mode 100644 index 000000000..5933f7334 --- /dev/null +++ b/internal/langserver/handlers/testdata/TestCompletion_payload_value/expect.json @@ -0,0 +1,84 @@ +{ + "jsonrpc": "2.0", + "id": 3, + "result": { + "isIncomplete": false, + "items": [ + { + "label": "\"SystemAssigned\"", + "labelDetails": {}, + "kind": 12, + "documentation": { + "kind": "markdown", + "value": "Value: `SystemAssigned` \n" + }, + "sortText": "0SystemAssigned", + "insertTextFormat": 1, + "insertTextMode": 2, + "textEdit": { + "range": { + "start": { + "line": 6, + "character": 13 + }, + "end": { + "line": 6, + "character": 15 + } + }, + "newText": "\"SystemAssigned\"" + } + }, + { + "label": "\"SystemAssigned,UserAssigned\"", + "labelDetails": {}, + "kind": 12, + "documentation": { + "kind": "markdown", + "value": "Value: `SystemAssigned,UserAssigned` \n" + }, + "sortText": "0SystemAssigned,UserAssigned", + "insertTextFormat": 1, + "insertTextMode": 2, + "textEdit": { + "range": { + "start": { + "line": 6, + "character": 13 + }, + "end": { + "line": 6, + "character": 15 + } + }, + "newText": "\"SystemAssigned,UserAssigned\"" + } + }, + { + "label": "\"UserAssigned\"", + "labelDetails": {}, + "kind": 12, + "documentation": { + "kind": "markdown", + "value": "Value: `UserAssigned` \n" + }, + "sortText": "0UserAssigned", + "insertTextFormat": 1, + "insertTextMode": 2, + "textEdit": { + "range": { + "start": { + "line": 6, + "character": 13 + }, + "end": { + "line": 6, + "character": 15 + } + }, + "newText": "\"UserAssigned\"" + } + } + ] + } +} \ No newline at end of file diff --git a/internal/langserver/handlers/testdata/TestCompletion_payload_value/main.tf b/internal/langserver/handlers/testdata/TestCompletion_payload_value/main.tf new file mode 100644 index 000000000..3b09ca13a --- /dev/null +++ b/internal/langserver/handlers/testdata/TestCompletion_payload_value/main.tf @@ -0,0 +1,10 @@ +resource "azapi_resource" "test" { + name = "acctest1774" + parent_id = azurerm_batch_account.test.id + type = "Microsoft.DataFactory/factories@2018-06-01" + payload = { + identity = { + type = "" + } + } +} diff --git a/internal/langserver/handlers/testdata/TestHover_payload_prop/expect.json b/internal/langserver/handlers/testdata/TestHover_payload_prop/expect.json new file mode 100644 index 000000000..41ac783a1 --- /dev/null +++ b/internal/langserver/handlers/testdata/TestHover_payload_prop/expect.json @@ -0,0 +1,20 @@ +{ + "jsonrpc": "2.0", + "id": 3, + "result": { + "contents": { + "kind": "markdown", + "value": "```\nidentity: Optional(object)\n```\nUser assigned identity to use to authenticate to customer's key vault. If not provided Managed Service Identity will be used." + }, + "range": { + "start": { + "line": 10, + "character": 8 + }, + "end": { + "line": 10, + "character": 16 + } + } + } +} \ No newline at end of file diff --git a/internal/langserver/handlers/testdata/TestHover_payload_prop/main.tf b/internal/langserver/handlers/testdata/TestHover_payload_prop/main.tf new file mode 100644 index 000000000..46c75dee4 --- /dev/null +++ b/internal/langserver/handlers/testdata/TestHover_payload_prop/main.tf @@ -0,0 +1,17 @@ +resource "azapi_resource" "test" { + name = "acctest1774" + parent_id = azurerm_batch_account.test.id + type = "Microsoft.DataFactory/factories@2018-06-01" + payload = { + identity = { + type = "SystemAssigned" + } + properties = { + encryption = { + identity = { + + } + } + } + } +} diff --git a/internal/langserver/handlers/testdata/TestHover_payload_propInArray/expect.json b/internal/langserver/handlers/testdata/TestHover_payload_propInArray/expect.json new file mode 100644 index 000000000..e30571215 --- /dev/null +++ b/internal/langserver/handlers/testdata/TestHover_payload_propInArray/expect.json @@ -0,0 +1,20 @@ +{ + "jsonrpc": "2.0", + "id": 3, + "result": { + "contents": { + "kind": "markdown", + "value": "```\norder: Optional(int)\n```\nRoute processing order." + }, + "range": { + "start": { + "line": 12, + "character": 10 + }, + "end": { + "line": 12, + "character": 15 + } + } + } +} \ No newline at end of file diff --git a/internal/langserver/handlers/testdata/TestHover_payload_propInArray/main.tf b/internal/langserver/handlers/testdata/TestHover_payload_propInArray/main.tf new file mode 100644 index 000000000..d8277b836 --- /dev/null +++ b/internal/langserver/handlers/testdata/TestHover_payload_propInArray/main.tf @@ -0,0 +1,25 @@ +resource "azapi_resource" "route" { + type = "Microsoft.AppPlatform/Spring/gateways/routeConfigs@2022-01-01-preview" + name = "henglu38" + parent_id = azapi_resource.gateway.id + + payload = { + properties = { + appResourceId = azapi_resource.app.id + routes = [ + { + description = "this description" // add some comment // add some comment + filters = ["StripPrefix=2", "RateLimit=1,1s"] // add some comment + order = 1 + predicates = ["Path=/api5/customer/**"] + ssoEnabled = false + tags = ["tag1", "tag2"] + title = "myApp route config" + tokenRelay = true + uri = "testuri" + } + ] + } + } + +} \ No newline at end of file diff --git a/internal/langserver/handlers/testdata/TestValidation_payload_missingRequiredProperty/main.tf b/internal/langserver/handlers/testdata/TestValidation_payload_missingRequiredProperty/main.tf new file mode 100644 index 000000000..a6031b272 --- /dev/null +++ b/internal/langserver/handlers/testdata/TestValidation_payload_missingRequiredProperty/main.tf @@ -0,0 +1,18 @@ +resource "azapi_resource" "test" { + name = "acctest1774" + parent_id = azurerm_batch_account.test.id + type = "Microsoft.DataFactory/factories@2018-06-01" + body = jsonencode({ + identity = { + type = "SystemAssigned" + } + properties = { + encryption = { + keyName = "" + identity = { + + } + } + } + }) +} diff --git a/internal/langserver/handlers/testdata/TestValidation_payload_missingRequiredPropertyInArrayItem/main.tf b/internal/langserver/handlers/testdata/TestValidation_payload_missingRequiredPropertyInArrayItem/main.tf new file mode 100644 index 000000000..c1cb2173c --- /dev/null +++ b/internal/langserver/handlers/testdata/TestValidation_payload_missingRequiredPropertyInArrayItem/main.tf @@ -0,0 +1,19 @@ + +resource "azapi_resource" "test" { + type = "Microsoft.ContainerService/managedClusters@2023-05-02-preview" + body = jsonencode({ + extendedLocation = { + + } + properties = { + agentPoolProfiles = [ + { + mode = "System" + }, + { + + } + ] + } + }) +} \ No newline at end of file diff --git a/internal/langserver/handlers/testdata/TestValidation_payload_notExpectedProperty/main.tf b/internal/langserver/handlers/testdata/TestValidation_payload_notExpectedProperty/main.tf new file mode 100644 index 000000000..4055ec97a --- /dev/null +++ b/internal/langserver/handlers/testdata/TestValidation_payload_notExpectedProperty/main.tf @@ -0,0 +1,12 @@ +resource "azapi_resource" "test" { + name = "acctest1774" + parent_id = azurerm_batch_account.test.id + type = "Microsoft.DataFactory/factories@2018-06-01" + body = jsonencode({ + identity1 = { + type = "SystemAssigned" + } + properties = { + } + }) +} diff --git a/internal/langserver/handlers/tfschema/body_candidates.go b/internal/langserver/handlers/tfschema/body_candidates.go index 7d0dfbf92..1446f7ca6 100644 --- a/internal/langserver/handlers/tfschema/body_candidates.go +++ b/internal/langserver/handlers/tfschema/body_candidates.go @@ -23,6 +23,43 @@ func bodyCandidates(data []byte, filename string, block *hclsyntax.Block, attrib } } + bodyDef := BodyDefinitionFromBlock(block) + if bodyDef == nil { + return nil + } + + hclNode := parser.JsonEncodeExpressionToHclNode(data, attribute.Expr) + if hclNode == nil { + return nil + } + + return buildCandidates(hclNode, filename, pos, bodyDef) +} + +func payloadCandidates(data []byte, filename string, block *hclsyntax.Block, attribute *hclsyntax.Attribute, pos hcl.Pos, property *Property) []lsp.CompletionItem { + if attribute.Expr != nil { + if _, ok := attribute.Expr.(*hclsyntax.LiteralValueExpr); ok && parser.ToLiteral(attribute.Expr) == nil { + if property != nil { + return property.ValueCandidatesFunc(nil, editRangeFromExprRange(attribute.Expr, pos)) + } + } + } + + bodyDef := BodyDefinitionFromBlock(block) + if bodyDef == nil { + return nil + } + + tokens, _ := hclsyntax.LexExpression(data[attribute.Expr.Range().Start.Byte:attribute.Expr.Range().End.Byte], filename, attribute.Expr.Range().Start) + hclNode := parser.BuildHclNode(tokens) + if hclNode == nil { + return nil + } + + return buildCandidates(hclNode, filename, pos, bodyDef) +} + +func BodyDefinitionFromBlock(block *hclsyntax.Block) types.TypeBase { typeValue := parser.ExtractAzureResourceType(block) if typeValue == nil { return nil @@ -47,16 +84,11 @@ func bodyCandidates(data []byte, filename string, block *hclsyntax.Block, attrib bodyDef = resourceFuncDef } } - - return buildCandidates(data, filename, attribute, pos, bodyDef) + return bodyDef } -func buildCandidates(data []byte, filename string, attribute *hclsyntax.Attribute, pos hcl.Pos, def types.TypeBase) []lsp.CompletionItem { +func buildCandidates(hclNode *parser.HclNode, filename string, pos hcl.Pos, def types.TypeBase) []lsp.CompletionItem { candidateList := make([]lsp.CompletionItem, 0) - hclNode := parser.JsonEncodeExpressionToHclNode(data, attribute.Expr) - if hclNode == nil { - return nil - } hclNodes := parser.HclNodeArraysOfPos(hclNode, pos) if len(hclNodes) == 0 { return nil diff --git a/internal/langserver/handlers/tfschema/candidates.go b/internal/langserver/handlers/tfschema/candidates.go index c35dd5686..e667c0135 100644 --- a/internal/langserver/handlers/tfschema/candidates.go +++ b/internal/langserver/handlers/tfschema/candidates.go @@ -161,6 +161,24 @@ func bodyJsonencodeFuncCandidate() lsp.CompletionItem { } } +func dynamicPlaceholderCandidate() lsp.CompletionItem { + return lsp.CompletionItem{ + Label: `{}`, + Kind: lsp.ValueCompletion, + Documentation: lsp.MarkupContent{ + Kind: "markdown", + Value: "dynamic attribute allows any valid HCL object.", + }, + SortText: `{}`, + InsertTextFormat: lsp.SnippetTextFormat, + InsertTextMode: lsp.AdjustIndentation, + TextEdit: &lsp.TextEdit{ + NewText: "{\n\t$0\n}", + }, + Command: constTriggerSuggestCommand(), + } +} + func resourceHttpMethodCandidates(_ *string, r lsp.Range) []lsp.CompletionItem { return valueCandidates([]string{ `"POST"`, diff --git a/internal/langserver/handlers/tfschema/init.go b/internal/langserver/handlers/tfschema/init.go index 19d36b077..5427d5265 100644 --- a/internal/langserver/handlers/tfschema/init.go +++ b/internal/langserver/handlers/tfschema/init.go @@ -79,6 +79,16 @@ func init() { GenericCandidatesFunc: bodyCandidates, }, + { + Name: "payload", + Modifier: "Optional", + Type: "dynamic", + Description: "A dynamic attribute that contains the request body used to create and update azure resource.", + CompletionNewText: `payload = $0`, + ValueCandidatesFunc: FixedValueCandidatesFunc([]lsp.CompletionItem{dynamicPlaceholderCandidate()}), + GenericCandidatesFunc: payloadCandidates, + }, + { Name: "tags", Modifier: "Optional", @@ -185,6 +195,16 @@ func init() { GenericCandidatesFunc: bodyCandidates, }, + { + Name: "payload", + Modifier: "Optional", + Type: "dynamic", + Description: "A dynamic attribute that contains the request body used to create and update azure resource.", + CompletionNewText: `payload = $0`, + ValueCandidatesFunc: FixedValueCandidatesFunc([]lsp.CompletionItem{dynamicPlaceholderCandidate()}), + GenericCandidatesFunc: payloadCandidates, + }, + { Name: "response_export_values", Modifier: "Optional", @@ -313,6 +333,16 @@ func init() { GenericCandidatesFunc: bodyCandidates, }, + { + Name: "payload", + Modifier: "Optional", + Type: "dynamic", + Description: "A dynamic attribute that contains the request body used to create and update azure resource.", + CompletionNewText: `payload = $0`, + ValueCandidatesFunc: FixedValueCandidatesFunc([]lsp.CompletionItem{dynamicPlaceholderCandidate()}), + GenericCandidatesFunc: payloadCandidates, + }, + { Name: "response_export_values", Modifier: "Optional", @@ -370,6 +400,16 @@ func init() { GenericCandidatesFunc: bodyCandidates, }, + { + Name: "payload", + Modifier: "Optional", + Type: "dynamic", + Description: "A dynamic attribute that contains the request body used to create and update azure resource.", + CompletionNewText: `payload = $0`, + ValueCandidatesFunc: FixedValueCandidatesFunc([]lsp.CompletionItem{dynamicPlaceholderCandidate()}), + GenericCandidatesFunc: payloadCandidates, + }, + { Name: "response_export_values", Modifier: "Optional", diff --git a/internal/langserver/handlers/validate/validate.go b/internal/langserver/handlers/validate/validate.go index 5fccb5ee0..92327dd6c 100644 --- a/internal/langserver/handlers/validate/validate.go +++ b/internal/langserver/handlers/validate/validate.go @@ -4,9 +4,9 @@ import ( "fmt" "strings" - "github.com/Azure/azapi-lsp/internal/azure" "github.com/Azure/azapi-lsp/internal/azure/types" "github.com/Azure/azapi-lsp/internal/langserver/diagnostics" + "github.com/Azure/azapi-lsp/internal/langserver/handlers/tfschema" "github.com/Azure/azapi-lsp/internal/parser" "github.com/Azure/azapi-lsp/internal/utils" "github.com/hashicorp/hcl/v2" @@ -54,41 +54,32 @@ func ValidateBlock(src []byte, block *hclsyntax.Block) hcl.Diagnostics { return nil } } - attribute := parser.AttributeWithName(block, "body") - if attribute == nil { - return nil - } typeValue := parser.ExtractAzureResourceType(block) if typeValue == nil { return nil } - var bodyDef types.TypeBase - def, err := azure.GetResourceDefinitionByResourceType(*typeValue) - if err != nil || def == nil { + bodyDef := tfschema.BodyDefinitionFromBlock(block) + if bodyDef == nil { return nil } - bodyDef = def - if len(block.Labels) >= 2 && block.Labels[0] == "azapi_resource_action" { - parts := strings.Split(*typeValue, "@") - if len(parts) != 2 { - return nil - } - actionName := parser.ExtractAction(block) - if actionName != nil && len(*actionName) != 0 { - resourceFuncDef, err := azure.GetResourceFunction(parts[0], parts[1], *actionName) - if err != nil || resourceFuncDef == nil { - return nil - } - bodyDef = resourceFuncDef - } - } - hclNode := parser.JsonEncodeExpressionToHclNode(src, attribute.Expr) - if hclNode == nil { + var attribute *hclsyntax.Attribute + var hclNode *parser.HclNode + if bodyAttribute := parser.AttributeWithName(block, "body"); bodyAttribute != nil { + attribute = bodyAttribute + hclNode = parser.JsonEncodeExpressionToHclNode(src, attribute.Expr) + } + if payloadAttribute := parser.AttributeWithName(block, "payload"); payloadAttribute != nil { + attribute = payloadAttribute + tokens, _ := hclsyntax.LexExpression(src[attribute.Expr.Range().Start.Byte:attribute.Expr.Range().End.Byte], "", attribute.Expr.Range().Start) + hclNode = parser.BuildHclNode(tokens) + } + if attribute == nil || hclNode == nil { return nil } + if dummy, ok := hclNode.Children["dummy"]; ok { dummy.KeyRange = attribute.NameRange if nameAttribute := parser.AttributeWithName(block, "name"); nameAttribute != nil { diff --git a/internal/langserver/handlers/validate/validate_test.go b/internal/langserver/handlers/validate/validate_test.go index be6e6787a..348bf7354 100644 --- a/internal/langserver/handlers/validate/validate_test.go +++ b/internal/langserver/handlers/validate/validate_test.go @@ -54,6 +54,18 @@ func TestValidation_missingRequiredProperty(t *testing.T) { } } +func TestValidation_payload_missingRequiredProperty(t *testing.T) { + config, err := os.ReadFile(fmt.Sprintf("../testdata/%s/main.tf", t.Name())) + if err != nil { + t.Fatal(err) + } + + _, diag := ValidateFile(config, "main.tf") + if len(diag) != 1 || diag[0].Summary != "`vaultBaseUrl` is required, but no definition was found" { + t.Errorf("expect 1 diagnostics, but got %v", diag) + } +} + func TestValidation_notExpectedProperty(t *testing.T) { config, err := os.ReadFile(fmt.Sprintf("../testdata/%s/main.tf", t.Name())) if err != nil { @@ -66,6 +78,18 @@ func TestValidation_notExpectedProperty(t *testing.T) { } } +func TestValidation_payload_notExpectedProperty(t *testing.T) { + config, err := os.ReadFile(fmt.Sprintf("../testdata/%s/main.tf", t.Name())) + if err != nil { + t.Fatal(err) + } + + _, diag := ValidateFile(config, "main.tf") + if len(diag) != 1 || diag[0].Summary != "`identity1` is not expected here. Do you mean `identity`? " { + t.Errorf("expect 1 diagnostics, but got %v", diag) + } +} + func TestValidation_update(t *testing.T) { config, err := os.ReadFile(fmt.Sprintf("../testdata/%s/main.tf", t.Name())) if err != nil { @@ -109,3 +133,23 @@ func TestValidation_missingRequiredPropertyInArrayItem(t *testing.T) { } } } + +func TestValidation_payload_missingRequiredPropertyInArrayItem(t *testing.T) { + config, err := os.ReadFile(fmt.Sprintf("../testdata/%s/main.tf", t.Name())) + if err != nil { + t.Fatal(err) + } + + _, diag := ValidateFile(config, "main.tf") + if len(diag) != 2 { + t.Errorf("expect 2 diagnostics, but got %v", diag) + } + for _, d := range diag { + if d.Summary != "`name` is required, but no definition was found" { + t.Errorf("expect `name` is required, but no definition was found, but got %v", d) + } + if d.Subject.Empty() { + t.Errorf("expect subject is not empty, but got %v", d) + } + } +}