diff --git a/readme/doc.go b/readme/doc.go index 25a7801..066cff6 100644 --- a/readme/doc.go +++ b/readme/doc.go @@ -181,7 +181,6 @@ func getDoc( parent, apiResponse, err := client.Doc.Get(IDPrefix+state.ParentDoc.ValueString(), options) if err != nil { - // failing here return state, apiResponse, errors.New(clientError(err, apiResponse)) } state.ParentDocSlug = types.StringValue(parent.Slug) diff --git a/readme/doc_resource.go b/readme/doc_resource.go index c713dc7..5c38bcd 100644 --- a/readme/doc_resource.go +++ b/readme/doc_resource.go @@ -6,10 +6,13 @@ import ( "reflect" "strings" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -18,7 +21,6 @@ import ( "github.com/liveoaklabs/readme-api-go-client/readme" "github.com/liveoaklabs/terraform-provider-readme/readme/frontmatter" - "github.com/liveoaklabs/terraform-provider-readme/readme/otherattributemodifier" ) // Ensure the implementation satisfies the expected interfaces. @@ -112,10 +114,80 @@ func (r docResource) ValidateConfig( } } +func (r *docResource) ModifyPlan( + ctx context.Context, + req resource.ModifyPlanRequest, + resp *resource.ModifyPlanResponse, +) { + plan := &docModel{} + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + + state := &docModel{} + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() || plan == nil { + return + } + + if state == nil { + plan.BodyClean = types.StringUnknown() + plan.BodyHTML = types.StringUnknown() + plan.Revision = types.Int64Unknown() + plan.UpdatedAt = types.StringUnknown() + plan.User = types.StringUnknown() + diags := resp.Plan.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + + return + } + + body := strings.TrimSpace(plan.Body.ValueString()) + + // Expand newline escape sequences. + body = strings.ReplaceAll(body, `\n`, "\n") + plan.BodyClean = types.StringValue(body) + + diags := resp.Plan.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + + // The 'algolia', 'revision', 'updated_at', and 'user' attributes are + // volatile and may show changes in the post-apply diff. If other + // attributes are changed, set these attributes to unknown to trigger a + // refresh. + if !state.BodyClean.Equal(plan.BodyClean) || + !state.BodyHTML.Equal(plan.BodyHTML) || + !state.Category.Equal(plan.Category) || + !state.CategorySlug.Equal(plan.CategorySlug) || + !state.Hidden.Equal(plan.Hidden) || + !state.Order.Equal(plan.Order) || + !state.ParentDoc.Equal(plan.ParentDoc) || + !state.ParentDocSlug.Equal(plan.ParentDocSlug) || + !state.Slug.Equal(plan.Slug) || + !state.Title.Equal(plan.Title) || + !state.Type.Equal(plan.Type) { + + plan.Revision = types.Int64Unknown() + plan.UpdatedAt = types.StringUnknown() + plan.User = types.StringUnknown() + + plan.Algolia = types.ObjectUnknown( + map[string]attr.Type{ + "record_count": types.Int64Type, + "publish_pending": types.BoolType, + "updated_at": types.StringType, + }, + ) + + } + + diags = resp.Plan.Set(ctx, plan) + resp.Diagnostics.Append(diags...) +} + // docPlanToParams maps plan attributes to a `readme.DocParams` struct to create or update a doc. func docPlanToParams(ctx context.Context, plan docModel) readme.DocParams { params := readme.DocParams{ - Body: strings.TrimSpace(plan.Body.ValueString()), + Body: plan.Body.ValueString(), Hidden: plan.Hidden.ValueBoolPointer(), Order: intPoint(int(plan.Order.ValueInt64())), Title: plan.Title.ValueString(), @@ -199,11 +271,18 @@ func (r *docResource) Create( if err != nil { resp.Diagnostics.AddError( "Unable to create doc.", - fmt.Sprintf( - "There was a problem retrieving the doc '%s' after creation: %s.", - doc.Slug, - err.Error(), - ), + fmt.Sprintf("There was a problem retrieving the doc '%s' after creation: %s.", doc.Slug, err.Error()), + ) + + return + } + + // Get the doc a second time to ensure the state is fully populated. + state, _, err = getDoc(r.client, ctx, doc.Slug, state, requestOpts) + if err != nil { + resp.Diagnostics.AddError( + "Unable to create doc.", + fmt.Sprintf("There was a problem retrieving the doc '%s' after creation: %s.", doc.Slug, err.Error()), ) return @@ -375,6 +454,21 @@ func (r *docResource) Update( return } + // Get the doc a second time to ensure the state is fully populated. + plan, _, err = getDoc(r.client, ctx, response.Slug, plan, requestOpts) + if err != nil { + resp.Diagnostics.AddError( + "Unable to update doc.", + fmt.Sprintf( + "There was a problem retrieving the doc '%s' after update: %s.", + response.Slug, + err.Error(), + ), + ) + + return + } + // Set refreshed state. resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) if resp.Diagnostics.HasError() { @@ -479,29 +573,18 @@ func (r *docResource) Schema( Attributes: map[string]schema.Attribute{ "record_count": schema.Int64Attribute{ Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, }, "publish_pending": schema.BoolAttribute{ Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, }, "updated_at": schema.StringAttribute{ Computed: true, - PlanModifiers: []planmodifier.String{ - otherattributemodifier.StringModifyString(path.Root("body"), "Body", true), - otherattributemodifier.StringModifyString(path.Root("body_html"), "BodyHTML", true), - otherattributemodifier.StringModifyString(path.Root("category"), "Category", true), - otherattributemodifier.StringModifyString(path.Root("category_slug"), "CategorySlug", true), - otherattributemodifier.BoolModifyString(path.Root("hidden"), "Hidden", true), - otherattributemodifier.Int64ModifyString(path.Root("order"), "Order", true), - otherattributemodifier.StringModifyString(path.Root("parent_doc"), "ParentDoc", true), - otherattributemodifier.StringModifyString( - path.Root("parent_doc_slug"), - "ParentDocSlug", - true, - ), - otherattributemodifier.StringModifyString(path.Root("slug"), "Slug", true), - otherattributemodifier.StringModifyString(path.Root("title"), "Title", true), - otherattributemodifier.StringModifyString(path.Root("type"), "Type", true), - }, }, }, }, @@ -683,10 +766,16 @@ func (r *docResource) Schema( "is_api": schema.BoolAttribute{ Description: "Identifies if a doc is an API doc or not.", Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, }, "is_reference": schema.BoolAttribute{ Description: "Identifies if a doc is a reference doc or not.", Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, }, "link_external": schema.BoolAttribute{ Description: "Identifies a doc's link as external or not.", @@ -789,23 +878,13 @@ func (r *docResource) Schema( "project": schema.StringAttribute{ Description: "The ID of the project the doc is in.", Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "revision": schema.Int64Attribute{ Description: "A number that is incremented upon doc updates.", Computed: true, - PlanModifiers: []planmodifier.Int64{ - otherattributemodifier.StringModifyInt64(path.Root("body"), "Body", true), - otherattributemodifier.StringModifyInt64(path.Root("body_html"), "BodyHTML", true), - otherattributemodifier.StringModifyInt64(path.Root("category"), "Category", true), - otherattributemodifier.StringModifyInt64(path.Root("category_slug"), "CategorySlug", true), - otherattributemodifier.BoolModifyInt64(path.Root("hidden"), "Hidden", true), - otherattributemodifier.Int64ModifyInt64(path.Root("order"), "Order", true), - otherattributemodifier.StringModifyInt64(path.Root("parent_doc"), "ParentDoc", true), - otherattributemodifier.StringModifyInt64(path.Root("parent_doc_slug"), "ParentDocSlug", true), - otherattributemodifier.StringModifyInt64(path.Root("slug"), "Slug", true), - otherattributemodifier.StringModifyInt64(path.Root("title"), "Title", true), - otherattributemodifier.StringModifyInt64(path.Root("type"), "Type", true), - }, }, "slug": schema.StringAttribute{ Description: "The slug of the doc.", @@ -841,29 +920,10 @@ func (r *docResource) Schema( "updated_at": schema.StringAttribute{ Description: "The timestamp of when the doc was last updated.", Computed: true, - PlanModifiers: []planmodifier.String{ - otherattributemodifier.StringModifyString(path.Root("body"), "Body", true), - otherattributemodifier.StringModifyString(path.Root("body_html"), "BodyHTML", true), - otherattributemodifier.StringModifyString(path.Root("category"), "Category", true), - otherattributemodifier.StringModifyString(path.Root("category_slug"), "CategorySlug", true), - otherattributemodifier.BoolModifyString(path.Root("hidden"), "Hidden", true), - otherattributemodifier.Int64ModifyString(path.Root("order"), "Order", true), - otherattributemodifier.StringModifyString(path.Root("parent_doc"), "ParentDoc", true), - otherattributemodifier.StringModifyString(path.Root("parent_doc_slug"), "ParentDocSlug", true), - otherattributemodifier.StringModifyString(path.Root("slug"), "Slug", true), - otherattributemodifier.StringModifyString(path.Root("title"), "Title", true), - otherattributemodifier.StringModifyString(path.Root("type"), "Type", true), - }, }, "user": schema.StringAttribute{ Description: "The ID of the author of the doc in the web editor.", Computed: true, - PlanModifiers: []planmodifier.String{ - // The user attribute is volatile and may show changes in - // the post-apply diff. This effectively triggers it to - // refresh whenever the document is updated. - otherattributemodifier.StringModifyString(path.Root("updated_at"), "UpdatedAt", true), - }, }, "use_slug": schema.StringAttribute{ Description: "**Use with caution!** Create the doc resource by importing an existing doc by its slug. " + diff --git a/readme/doc_resource_test.go b/readme/doc_resource_test.go index be7bcc9..351d1a5 100644 --- a/readme/doc_resource_test.go +++ b/readme/doc_resource_test.go @@ -4,6 +4,7 @@ package readme import ( "fmt" "regexp" + "strings" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" @@ -35,7 +36,7 @@ func TestDocResource(t *testing.T) { // Mock the request to create the resource. gock.New(testURL).Post("/docs").Times(1).Reply(201).JSON(mockDoc) // Mock the request to get and refresh the resource. - gock.New(testURL).Get("/docs/" + mockDoc.Slug).Times(2).Reply(200).JSON(mockDoc) + gock.New(testURL).Get("/docs/" + mockDoc.Slug).Times(3).Reply(200).JSON(mockDoc) }, Check: docResourceCommonChecks(mockDoc, ""), }, @@ -68,7 +69,7 @@ func TestDocResource(t *testing.T) { // Mock the request to create the resource. gock.New(testURL).Post("/docs").Times(1).Reply(201).JSON(mockDoc) // Mock the request to get and refresh the resource. - gock.New(testURL).Get("/docs/" + mockDoc.Slug).Times(2).Reply(200).JSON(mockDoc) + gock.New(testURL).Get("/docs/" + mockDoc.Slug).Times(3).Reply(200).JSON(mockDoc) gock.New(testURL).Delete("/docs/" + mockDoc.Slug).Times(1).Reply(204) }, @@ -308,7 +309,8 @@ func TestDocResource_FrontMatter(t *testing.T) { --- title: ignored --- - This is a document.`, + This is a document. + `, }, attributes: fmt.Sprintf( ` @@ -326,9 +328,8 @@ func TestDocResource_FrontMatter(t *testing.T) { --- title: %s --- - This is a document.`, - mockDoc.Title, - ), + This is a document. + `, mockDoc.Title), }, attributes: fmt.Sprintf( ` @@ -551,7 +552,8 @@ func TestDocResource_FrontMatter(t *testing.T) { --- type: %s --- - This is a document.`, mockDoc.Type, + This is a document. + `, mockDoc.Type, ), }, attributes: fmt.Sprintf( @@ -569,6 +571,7 @@ func TestDocResource_FrontMatter(t *testing.T) { // Set up the doc we're testing, derived from the common mock with the expected attributes overridden. expect := mockDoc expect.Body = removeIndents(testCase.expect.Body) + expect.Body = strings.Trim(expect.Body, "\n") if testCase.expect.Hidden != nil { expect.Hidden = *testCase.expect.Hidden @@ -588,11 +591,11 @@ func TestDocResource_FrontMatter(t *testing.T) { Config: providerConfig + fmt.Sprintf( ` resource "readme_doc" "test" { - body = chomp("%s") - %s + body = chomp("%s") + %s } `, - replaceNewlines(expect.Body), + escapeNewlines(expect.Body), testCase.attributes, ), PreConfig: func() { @@ -672,16 +675,16 @@ func TestDocResource_User_Attribute_Changes(t *testing.T) { Config: providerConfig + fmt.Sprintf(` resource "readme_doc" "test" { title = "%s" - body = "body" + body = "%s" category = "%s" type = "%s" }`, - expectedDoc.Title, expectedDoc.Category, expectedDoc.Type, + expectedDoc.Title, expectedDoc.Body, expectedDoc.Category, expectedDoc.Type, ), PreConfig: func() { docCommonGocks() gock.New(testURL).Post("/docs").Times(1).Reply(201).JSON(expectedDoc) - gock.New(testURL).Get("/docs/" + expectedDoc.Slug).Times(2).Reply(200).JSON(expectedDoc) + gock.New(testURL).Get("/docs/" + expectedDoc.Slug).Times(3).Reply(200).JSON(expectedDoc) }, Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr( @@ -702,12 +705,13 @@ func TestDocResource_User_Attribute_Changes(t *testing.T) { updatedDoc.Title, updatedDoc.Category, updatedDoc.Type, ), PreConfig: func() { + updatedDoc.Body = "updated body" docCommonGocks() // First request responds with the original user. gock.New(testURL).Get("/docs/" + expectedDoc.Slug).Times(1).Reply(200).JSON(expectedDoc) gock.New(testURL).Put("/docs").Times(1).Reply(200).JSON(updatedDoc) // Post-update request has the updated user. - gock.New(testURL).Get("/docs/" + updatedDoc.Slug).Times(2).Reply(200).JSON(updatedDoc) + gock.New(testURL).Get("/docs/" + updatedDoc.Slug).Times(3).Reply(200).JSON(updatedDoc) gock.New(testURL).Delete("/docs/" + updatedDoc.Slug).Times(1).Reply(204) }, Check: resource.ComposeAggregateTestCheckFunc( @@ -750,7 +754,7 @@ func TestDocResource_Hidden_Attribute_Changes(t *testing.T) { docCommonGocks() gock.New(testURL).Post("/docs").Times(1).Reply(201).JSON(expectedDoc) - gock.New(testURL).Get("/docs/" + expectedDoc.Slug).Times(2).Reply(200).JSON(expectedDoc) + gock.New(testURL).Get("/docs/" + expectedDoc.Slug).Times(3).Reply(200).JSON(expectedDoc) }, Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr( @@ -765,7 +769,7 @@ func TestDocResource_Hidden_Attribute_Changes(t *testing.T) { Config: providerConfig + fmt.Sprintf(` resource "readme_doc" "test" { title = "%s" - body = "body" + body = "body" category = "%s" hidden = true type = "%s" @@ -793,19 +797,19 @@ func TestDocResource_Hidden_Attribute_Changes(t *testing.T) { Config: providerConfig + fmt.Sprintf(` resource "readme_doc" "test" { title = "%s" - body = "---\nhidden: false\n---\nbody" + body = "---\nhidden: false\n---\nbody" category = "%s" type = "%s" }`, expectedDoc.Title, expectedDoc.Category, expectedDoc.Type, ), PreConfig: func() { - expectedDoc.Body = `---\nhidden: false\n---\nbody` + expectedDoc.Body = "---\nhidden: false\n---\nbody" expectedDoc.Hidden = false docCommonGocks() gock.New(testURL).Put("/docs/" + expectedDoc.Slug).Times(1).Reply(200).JSON(expectedDoc) - gock.New(testURL).Get("/docs/" + expectedDoc.Slug).Times(3).Reply(200).JSON(expectedDoc) + gock.New(testURL).Get("/docs/" + expectedDoc.Slug).Times(4).Reply(200).JSON(expectedDoc) }, Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr( @@ -820,7 +824,7 @@ func TestDocResource_Hidden_Attribute_Changes(t *testing.T) { Config: providerConfig + fmt.Sprintf(` resource "readme_doc" "test" { title = "%s" - body = "body" + body = "body" category = "%s" hidden = true type = "%s" @@ -848,7 +852,7 @@ func TestDocResource_Hidden_Attribute_Changes(t *testing.T) { Config: providerConfig + fmt.Sprintf(` resource "readme_doc" "test" { title = "%s" - body = "body" + body = "body" category = "%s" hidden = false type = "%s" @@ -876,7 +880,7 @@ func TestDocResource_Hidden_Attribute_Changes(t *testing.T) { Config: providerConfig + fmt.Sprintf(` resource "readme_doc" "test" { title = "%s" - body = "body" + body = "body" category = "%s" type = "%s" }`, @@ -929,11 +933,12 @@ func TestDocResource_Order_Attribute_Changes(t *testing.T) { expectedDoc.Title, expectedDoc.Body, expectedDoc.Category, expectedDoc.Type, ), PreConfig: func() { + gock.OffAll() expectedDoc.Order = 1 docCommonGocks() gock.New(testURL).Post("/docs").Times(1).Reply(201).JSON(expectedDoc) - gock.New(testURL).Get("/docs/" + expectedDoc.Slug).Times(2).Reply(200).JSON(expectedDoc) + gock.New(testURL).Get("/docs/" + expectedDoc.Slug).Times(3).Reply(200).JSON(expectedDoc) }, Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr( @@ -948,7 +953,7 @@ func TestDocResource_Order_Attribute_Changes(t *testing.T) { Config: providerConfig + fmt.Sprintf(` resource "readme_doc" "test" { title = "%s" - body = "%s" + body = "%s" category = "%s" order = 2 type = "%s" @@ -975,19 +980,20 @@ func TestDocResource_Order_Attribute_Changes(t *testing.T) { Config: providerConfig + fmt.Sprintf(` resource "readme_doc" "test" { title = "%s" - body = "---\norder: 3\n---\nbody" + body = "---\norder: 3\n---\nbody" category = "%s" type = "%s" }`, expectedDoc.Title, expectedDoc.Category, expectedDoc.Type, ), PreConfig: func() { - expectedDoc.Body = `---\norder: 3\n---\nbody` + gock.OffAll() + expectedDoc.Body = "---\norder: 3\n---\nbody" expectedDoc.Order = 3 docCommonGocks() gock.New(testURL).Put("/docs/" + expectedDoc.Slug).Times(1).Reply(200).JSON(expectedDoc) - gock.New(testURL).Get("/docs/" + expectedDoc.Slug).Times(3).Reply(200).JSON(expectedDoc) + gock.New(testURL).Get("/docs/" + expectedDoc.Slug).Times(4).Reply(200).JSON(expectedDoc) }, Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr( @@ -1002,7 +1008,7 @@ func TestDocResource_Order_Attribute_Changes(t *testing.T) { Config: providerConfig + fmt.Sprintf(` resource "readme_doc" "test" { title = "%s" - body = "%s" + body = "%s" category = "%s" order = 4 type = "%s" @@ -1015,7 +1021,7 @@ func TestDocResource_Order_Attribute_Changes(t *testing.T) { docCommonGocks() gock.New(testURL).Put("/docs/" + expectedDoc.Slug).Times(1).Reply(200).JSON(expectedDoc) - gock.New(testURL).Get("/docs/" + expectedDoc.Slug).Times(3).Reply(200).JSON(expectedDoc) + gock.New(testURL).Get("/docs/" + expectedDoc.Slug).Times(4).Reply(200).JSON(expectedDoc) }, Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr( @@ -1030,7 +1036,7 @@ func TestDocResource_Order_Attribute_Changes(t *testing.T) { Config: providerConfig + fmt.Sprintf(` resource "readme_doc" "test" { title = "%s" - body = "%s" + body = "%s" category = "%s" type = "%s" }`, @@ -1064,7 +1070,7 @@ func TestDocResource_Order_FrontMatter_Changes(t *testing.T) { defer gock.OffAll() expectedDoc := mockDoc - expectedDoc.Body = `---\norder: 1\n---\nbody` + expectedDoc.Body = "---\norder: 1\n---\nbody" expectedDoc.Order = 1 resource.Test(t, resource.TestCase{ @@ -1076,16 +1082,17 @@ func TestDocResource_Order_FrontMatter_Changes(t *testing.T) { Config: providerConfig + fmt.Sprintf(` resource "readme_doc" "test" { title = "%s" - body = "%s" + body = "---\norder: 1\n---\nbody" category = "%s" type = "%s" }`, - expectedDoc.Title, expectedDoc.Body, expectedDoc.Category, expectedDoc.Type, + expectedDoc.Title, expectedDoc.Category, expectedDoc.Type, ), PreConfig: func() { + gock.OffAll() docCommonGocks() gock.New(testURL).Post("/docs").Times(1).Reply(201).JSON(expectedDoc) - gock.New(testURL).Get("/docs/" + expectedDoc.Slug).Times(2).Reply(200).JSON(expectedDoc) + gock.New(testURL).Get("/docs/" + expectedDoc.Slug).Times(3).Reply(200).JSON(expectedDoc) }, Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr( @@ -1100,20 +1107,20 @@ func TestDocResource_Order_FrontMatter_Changes(t *testing.T) { Config: providerConfig + fmt.Sprintf(` resource "readme_doc" "test" { title = "%s" - #body = "%s" - body = "---\norder: 2\n---\nbody" + body = "---\norder: 2\n---\nbody" category = "%s" type = "%s" }`, - expectedDoc.Title, expectedDoc.Body, expectedDoc.Category, expectedDoc.Type, + expectedDoc.Title, expectedDoc.Category, expectedDoc.Type, ), PreConfig: func() { - expectedDoc.Body = `---\norder: 2\n---\nbody` + gock.OffAll() + expectedDoc.Body = "---\norder: 2\n---\nbody" expectedDoc.Order = 2 docCommonGocks() gock.New(testURL).Put("/docs/" + expectedDoc.Slug).Times(1).Reply(200).JSON(expectedDoc) - gock.New(testURL).Get("/docs/" + expectedDoc.Slug).Times(3).Reply(200).JSON(expectedDoc) + gock.New(testURL).Get("/docs/" + expectedDoc.Slug).Times(4).Reply(200).JSON(expectedDoc) }, Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr( @@ -1128,19 +1135,20 @@ func TestDocResource_Order_FrontMatter_Changes(t *testing.T) { Config: providerConfig + fmt.Sprintf(` resource "readme_doc" "test" { title = "%s" - body = "body" + body = "body" category = "%s" type = "%s" }`, expectedDoc.Title, expectedDoc.Category, expectedDoc.Type, ), PreConfig: func() { + gock.OffAll() expectedDoc.Body = `body` expectedDoc.Order = 999 docCommonGocks() gock.New(testURL).Put("/docs/" + expectedDoc.Slug).Times(1).Reply(200).JSON(expectedDoc) - gock.New(testURL).Get("/docs/" + expectedDoc.Slug).Times(3).Reply(200).JSON(expectedDoc) + gock.New(testURL).Get("/docs/" + expectedDoc.Slug).Times(4).Reply(200).JSON(expectedDoc) // Post-test deletion gock.New(testURL).Delete("/docs/" + expectedDoc.Slug).Times(1).Reply(204) @@ -1189,7 +1197,7 @@ func TestDocRenamedSlugResource(t *testing.T) { // Mock the request to create the resource. gock.New(testURL).Post("/docs").Times(1).Reply(201).JSON(mockDoc) // Mock the request to get and refresh the resource. - gock.New(testURL).Get("/docs/" + mockDoc.Slug).Times(2).Reply(200).JSON(mockDoc) + gock.New(testURL).Get("/docs/" + mockDoc.Slug).Times(3).Reply(200).JSON(mockDoc) }, Check: docResourceCommonChecks(mockDoc, ""), }, @@ -1226,7 +1234,7 @@ func TestDocRenamedSlugResource(t *testing.T) { // The matched doc is requested from the search results. // It's also requested again after the rename. - gock.New(testURL).Get("/docs/" + "new-slug").Times(3).Reply(200).JSON(renamed) + gock.New(testURL).Get("/docs/" + "new-slug").Times(4).Reply(200).JSON(renamed) // An update is triggered to match state with the new slug. gock.New(testURL).Put("/docs/" + "new-slug").Times(1).Reply(200).JSON(renamed) diff --git a/readme/doc_test.go b/readme/doc_test.go index 21043ab..22092c6 100644 --- a/readme/doc_test.go +++ b/readme/doc_test.go @@ -182,7 +182,8 @@ func docCommonGocks() { Reply(200). JSON(mockCategory) // Lookup version. - gock.New(testURL).Get("/version").Persist().Reply(200).JSON(mockVersionList) + gock.New(testURL).Get("/version").Times(1).Reply(200).JSON(mockVersionList) + gock.New(testURL).Get("/version/" + mockVersionList[0].VersionClean).Times(1).Reply(200).JSON(mockVersion) // List of docs to match parent doc. gock.New(testURL). Post("/docs"). diff --git a/readme/otherattributemodifier/bool.go b/readme/otherattributemodifier/bool.go deleted file mode 100644 index b2bca2b..0000000 --- a/readme/otherattributemodifier/bool.go +++ /dev/null @@ -1,265 +0,0 @@ -package otherattributemodifier - -// Plan modifier for planning a change for an attribute if another specified -// *bool* attribute changes. - -import ( - "context" - "fmt" - "reflect" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/liveoaklabs/terraform-provider-readme/readme/frontmatter" -) - -// otherBoolChanged is a plan modifier that plans a change for an -// attribute if another specified *bool* attribute is changed. -type otherBoolChanged struct { - otherAttribute path.Path - otherField string - checkFrontmatter bool - req interface{} - resp interface{} -} - -// BoolModifyString is a plan modifier to flag a *string* attribute -// for change if another specified *bool* attribute changes. -// The attribute argument is the path to the attribute to check. -// The otherField argument is the name of the Go struct field in the -// frontmatter. -// The checkFrontmatter argument is a boolean to indicate whether to -// check the frontmatter if the attribute is not set. -func BoolModifyString( - attribute path.Path, - otherField string, - checkFrontmatter bool, -) planmodifier.String { - return otherBoolChanged{ - otherAttribute: attribute, - otherField: otherField, - checkFrontmatter: checkFrontmatter, - } -} - -// BoolModifyInt64 is a plan modifier to flag an *int64* attribute for -// change if another specified *bool* attribute changes. -// The attribute argument is the path to the attribute to check. -// The otherField argument is the name of the Go struct field in the -// frontmatter. -// The checkFrontmatter argument is a boolean to indicate whether to -// check the frontmatter if the attribute is not set. -func BoolModifyInt64( - attribute path.Path, - otherField string, - checkFrontmatter bool, -) planmodifier.Int64 { - return otherBoolChanged{ - otherAttribute: attribute, - otherField: otherField, - checkFrontmatter: checkFrontmatter, - } -} - -// BoolModifyBool is a plan modifier to flag a *bool* attribute for -// change if another specified *bool* attribute changes. -// The attribute argument is the path to the attribute to check. -// The otherField argument is the name of the Go struct field in the -// frontmatter. -// The checkFrontmatter argument is a boolean to indicate whether to -// check the frontmatter if the attribute is not set. -func BoolModifyBool( - attribute path.Path, - otherField string, - checkFrontmatter bool, -) planmodifier.Bool { - return otherBoolChanged{ - otherAttribute: attribute, - otherField: otherField, - checkFrontmatter: checkFrontmatter, - } -} - -// Description returns a plain text description of the modifier's behavior. -func (m otherBoolChanged) Description(ctx context.Context) string { - return "If another bool attribute is changed, this attribute will be changed." -} - -// MarkdownDescription returns a markdown formatted description of the -// modifier's behavior. -func (m otherBoolChanged) MarkdownDescription(ctx context.Context) string { - return m.Description(ctx) -} - -// Load the config, plan, state, and body plan values based on the type. -// The config is loaded to know whether to check frontmatter if the -// attribute is not set. Load the plan and state to compare across values. -// The body plan value is loaded to check frontmatter if the attribute is not set. -func (m otherBoolChanged) loadValues( - ctx context.Context, -) (types.Bool, types.Bool, types.Bool, types.String, diag.Diagnostics) { - var bodyValue types.String - var configValue, planValue, stateValue types.Bool - var diags diag.Diagnostics - - switch m.req.(type) { - case planmodifier.StringRequest: - req := m.req.(planmodifier.StringRequest) - resp := m.resp.(*planmodifier.StringResponse) - rDiag := resp.Diagnostics - - rDiag.Append(req.Config.GetAttribute(ctx, m.otherAttribute, &configValue)...) - rDiag.Append(req.State.GetAttribute(ctx, m.otherAttribute, &stateValue)...) - rDiag.Append(req.Plan.GetAttribute(ctx, m.otherAttribute, &planValue)...) - rDiag.Append(req.Plan.GetAttribute(ctx, path.Root("body"), &bodyValue)...) - case planmodifier.Int64Request: - req := m.req.(planmodifier.Int64Request) - resp := m.resp.(*planmodifier.Int64Response) - rDiag := resp.Diagnostics - - rDiag.Append(req.State.GetAttribute(ctx, m.otherAttribute, &stateValue)...) - rDiag.Append(req.Config.GetAttribute(ctx, m.otherAttribute, &configValue)...) - rDiag.Append(req.Plan.GetAttribute(ctx, m.otherAttribute, &planValue)...) - rDiag.Append(req.Plan.GetAttribute(ctx, path.Root("body"), &bodyValue)...) - case planmodifier.BoolRequest: - req := m.req.(planmodifier.BoolRequest) - resp := m.resp.(*planmodifier.BoolResponse) - rDiag := resp.Diagnostics - - rDiag.Append(req.State.GetAttribute(ctx, m.otherAttribute, &stateValue)...) - rDiag.Append(req.Config.GetAttribute(ctx, m.otherAttribute, &configValue)...) - rDiag.Append(req.Plan.GetAttribute(ctx, m.otherAttribute, &planValue)...) - rDiag.Append(req.Plan.GetAttribute(ctx, path.Root("body"), &bodyValue)...) - } - - return configValue, stateValue, planValue, bodyValue, diags -} - -// modifyAttribute is a helper function to modify the attribute based on the -// type of the request and response. -// -// The repetition is necessary because the request and response types are -// different for each type. -func (m otherBoolChanged) modifyAttribute(ctx context.Context) { - var isChanged bool - var diags diag.Diagnostics - - // Load the config, plan, state, and body plan values based on the type. - otherConfigValue, otherStateValue, otherPlanValue, bodyPlanValue, diags := m.loadValues(ctx) - - // If the attribute isn't set, check the body front matter. - if m.checkFrontmatter && otherConfigValue.IsNull() { - value, errStr := frontmatter.GetValue(ctx, bodyPlanValue.ValueString(), m.otherField) - if errStr != "" { - diags.AddError("Error parsing front matter.", errStr) - - return - } - - // If the value from frontmatter is not empty, compare it to the current state. - if value != (reflect.Value{}) { - fmValue := value.Interface().(*bool) - tflog.Debug(ctx, fmt.Sprintf( - "%s was found in frontmatter with value %v", - m.otherAttribute, *fmValue)) - - // If the value from frontmatter is different from the current - // plan, mark this attribute as changed. - isChanged = *fmValue != otherPlanValue.ValueBool() - } else { - tflog.Debug(ctx, fmt.Sprintf( - "value for %s was not found in frontmatter", - m.otherAttribute)) - } - } else { - // If the attribute is set, compare it to the current state and ignore - // the frontmatter. - tflog.Debug(ctx, "otherBoolChanged: not checking front matter") - isChanged = otherConfigValue != otherStateValue && !otherStateValue.IsNull() - } - - tflog.Debug(ctx, fmt.Sprintf( - "otherBoolChanged: %s otherConfigValue (%s) otherStateValue (%s)", - m.otherAttribute, otherConfigValue, otherStateValue)) - - // If the other attribute is changed, set this attribute unknown to trigger - // an update. Otherwise, set it to the current plan value. - switch m.req.(type) { - case planmodifier.StringRequest: - resp := m.resp.(*planmodifier.StringResponse) - req := m.req.(planmodifier.StringRequest) - - if isChanged { - resp.PlanValue = types.StringUnknown() - - return - } - - resp.PlanValue = req.PlanValue - case planmodifier.Int64Request: - resp := m.resp.(*planmodifier.Int64Response) - req := m.req.(planmodifier.Int64Request) - - if isChanged { - resp.PlanValue = types.Int64Unknown() - - return - } - - resp.PlanValue = req.PlanValue - case planmodifier.BoolRequest: - resp := m.resp.(*planmodifier.BoolResponse) - req := m.req.(planmodifier.BoolRequest) - - if isChanged { - resp.PlanValue = types.BoolUnknown() - - return - } - - resp.PlanValue = req.PlanValue - default: - tflog.Info(ctx, fmt.Sprintf( - "otherBoolChanged: unknown request type %T", m.req)) - } -} - -// PlanModifyString implements a modifier for planning a change for an -// attribute if another specified *bool* attribute changes. -func (m otherBoolChanged) PlanModifyString( - ctx context.Context, - req planmodifier.StringRequest, - resp *planmodifier.StringResponse, -) { - m.req = req - m.resp = resp - m.modifyAttribute(ctx) -} - -// PlanModifyInt64 implements a modifier for planning a change for an -// *int64* attribute if another specified *bool* attribute changes. -// The string attribute is optionally cheked for a value in the frontmatter. -func (m otherBoolChanged) PlanModifyInt64( - ctx context.Context, - req planmodifier.Int64Request, - resp *planmodifier.Int64Response, -) { - m.req = req - m.resp = resp - m.modifyAttribute(ctx) -} - -// PlanModifyBool implements a modifier for planning a change for a *bool* -// attribute if another specified *bool* attribute changes. -func (m otherBoolChanged) PlanModifyBool( - ctx context.Context, - req planmodifier.BoolRequest, - resp *planmodifier.BoolResponse, -) { - m.req = req - m.resp = resp - m.modifyAttribute(ctx) -} diff --git a/readme/otherattributemodifier/int64.go b/readme/otherattributemodifier/int64.go deleted file mode 100644 index c5a8651..0000000 --- a/readme/otherattributemodifier/int64.go +++ /dev/null @@ -1,249 +0,0 @@ -package otherattributemodifier - -// Plan modifier for planning a change for an attribute if another specified -// *int64* attribute changes. - -import ( - "context" - "fmt" - "reflect" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/liveoaklabs/terraform-provider-readme/readme/frontmatter" -) - -// otherInt64Changed is a plan modifier that plans a change for an -// attribute if another specified *int64* attribute is changed. -type otherInt64Changed struct { - otherAttribute path.Path - otherField string - checkFrontmatter bool - req interface{} - resp interface{} -} - -// Int64ModifyString is a plan modifier to flag a *string* attribute -// for change if another specified *int64* attribute changes. -// The attribute argument is the path to the attribute to check. -// The otherField argument is the name of the Go struct field in the -// frontmatter. -// The checkFrontmatter argument is a boolean to indicate whether to -// check the frontmatter if the attribute is not set. -func Int64ModifyString( - attribute path.Path, - otherField string, - checkFrontmatter bool, -) planmodifier.String { - return otherInt64Changed{ - otherAttribute: attribute, - otherField: otherField, - checkFrontmatter: checkFrontmatter, - } -} - -// Int64ModifyInt64 is a plan modifier to flag an *int64* attribute for -// change if another specified *int64* attribute changes. -// The attribute argument is the path to the attribute to check. -// The otherField argument is the name of the Go struct field in the -// frontmatter. -// The checkFrontmatter argument is a boolean to indicate whether to -// check the frontmatter if the attribute is not set. -func Int64ModifyInt64( - attribute path.Path, - otherField string, - checkFrontmatter bool, -) planmodifier.Int64 { - return otherInt64Changed{ - otherAttribute: attribute, - otherField: otherField, - checkFrontmatter: checkFrontmatter, - } -} - -// Description returns a plain text description of the modifier's behavior. -func (m otherInt64Changed) Description(ctx context.Context) string { - return "If another int64 attribute is changed, this attribute will be changed." -} - -// MarkdownDescription returns a markdown formatted description of the -// modifier's behavior. -func (m otherInt64Changed) MarkdownDescription(ctx context.Context) string { - return m.Description(ctx) -} - -// Load the config, plan, state, and body plan values based on the type. -// The config is loaded to know whether to check frontmatter if the -// attribute is not set. Load the plan and state to compare across values. -// The body plan value is loaded to check frontmatter if the attribute is not set. -func (m otherInt64Changed) loadValues( - ctx context.Context, -) (types.Int64, types.Int64, types.Int64, types.String, diag.Diagnostics) { - var bodyPlanValue types.String - var configValue, planValue, stateValue types.Int64 - var diags diag.Diagnostics - - switch m.req.(type) { - case planmodifier.StringRequest: - req := m.req.(planmodifier.StringRequest) - resp := m.resp.(*planmodifier.StringResponse) - rDiag := resp.Diagnostics - - rDiag.Append(req.Config.GetAttribute(ctx, m.otherAttribute, &configValue)...) - rDiag.Append(req.State.GetAttribute(ctx, m.otherAttribute, &stateValue)...) - rDiag.Append(req.Plan.GetAttribute(ctx, m.otherAttribute, &planValue)...) - rDiag.Append(req.Plan.GetAttribute(ctx, path.Root("body"), &bodyPlanValue)...) - case planmodifier.Int64Request: - req := m.req.(planmodifier.Int64Request) - resp := m.resp.(*planmodifier.Int64Response) - rDiag := resp.Diagnostics - - rDiag.Append(req.State.GetAttribute(ctx, m.otherAttribute, &stateValue)...) - rDiag.Append(req.Config.GetAttribute(ctx, m.otherAttribute, &configValue)...) - rDiag.Append(req.Plan.GetAttribute(ctx, m.otherAttribute, &planValue)...) - rDiag.Append(req.Plan.GetAttribute(ctx, path.Root("body"), &bodyPlanValue)...) - case planmodifier.BoolRequest: - req := m.req.(planmodifier.BoolRequest) - resp := m.resp.(*planmodifier.BoolResponse) - rDiag := resp.Diagnostics - - rDiag.Append(req.State.GetAttribute(ctx, m.otherAttribute, &stateValue)...) - rDiag.Append(req.Config.GetAttribute(ctx, m.otherAttribute, &configValue)...) - rDiag.Append(req.Plan.GetAttribute(ctx, m.otherAttribute, &planValue)...) - rDiag.Append(req.Plan.GetAttribute(ctx, path.Root("body"), &bodyPlanValue)...) - default: - tflog.Error(ctx, fmt.Sprintf( - "otherInt64Changed: unknown request type %T", m.req)) - } - - return configValue, stateValue, planValue, bodyPlanValue, diags -} - -// modifyAttribute is a helper function to modify the attribute based on the -// type of the request and response. -// -// The repetition is necessary because the request and response types are -// different for each type. -func (m otherInt64Changed) modifyAttribute(ctx context.Context) { - var isChanged bool - var diags diag.Diagnostics - - // Load the config, plan, state, and body plan values based on the type. - otherConfigValue, otherStateValue, otherPlanValue, bodyPlanValue, diags := m.loadValues(ctx) - - // If the attribute isn't set, check the body front matter. - if m.checkFrontmatter && otherConfigValue.IsNull() { - value, errStr := frontmatter.GetValue(ctx, bodyPlanValue.ValueString(), m.otherField) - if errStr != "" { - diags.AddError("Error parsing front matter.", errStr) - - return - } - - // If the value from frontmatter is not empty, compare it to the current state. - if value != (reflect.Value{}) { - tflog.Debug(ctx, fmt.Sprintf( - "%s was found in frontmatter with value %s", - m.otherAttribute, value)) - - // If the value from frontmatter is different from the current - // plan, mark this attribute as changed. - isChanged = value.Interface().(int64) != otherPlanValue.ValueInt64() - } else { - tflog.Debug(ctx, fmt.Sprintf( - "value for %s was not found in frontmatter", - m.otherAttribute)) - } - } else { - // If the attribute is set, compare it to the current state and ignore - // the frontmatter. - tflog.Debug(ctx, "otherInt64Changed: not checking front matter") - isChanged = otherConfigValue != otherStateValue && !otherStateValue.IsNull() - } - - tflog.Debug(ctx, fmt.Sprintf( - "otherInt64Changed: %s otherConfigValue (%s) otherStateValue (%s)", - m.otherAttribute, otherConfigValue, otherStateValue)) - - // If the other attribute is changed, set this attribute unknown to trigger - // an update. Otherwise, set it to the current plan value. - switch m.req.(type) { - case planmodifier.StringRequest: - resp := m.resp.(*planmodifier.StringResponse) - req := m.req.(planmodifier.StringRequest) - - if isChanged { - resp.PlanValue = types.StringUnknown() - - return - } - - resp.PlanValue = req.PlanValue - case planmodifier.Int64Request: - resp := m.resp.(*planmodifier.Int64Response) - req := m.req.(planmodifier.Int64Request) - - if isChanged { - resp.PlanValue = types.Int64Unknown() - - return - } - - resp.PlanValue = req.PlanValue - - case planmodifier.BoolRequest: - resp := m.resp.(*planmodifier.BoolResponse) - req := m.req.(planmodifier.BoolRequest) - - if isChanged { - resp.PlanValue = types.BoolUnknown() - - return - } - - resp.PlanValue = req.PlanValue - default: - tflog.Info(ctx, fmt.Sprintf( - "otherInt64Changed: unknown request type %T", m.req)) - } -} - -// PlanModifyString implements a modifier for planning a change for an -// attribute if another specified *int64* attribute changes. -func (m otherInt64Changed) PlanModifyString( - ctx context.Context, - req planmodifier.StringRequest, - resp *planmodifier.StringResponse, -) { - m.req = req - m.resp = resp - m.modifyAttribute(ctx) -} - -// PlanModifyInt64 implements a modifier for planning a change for an -// *int64* attribute if another specified *int64* attribute changes. -// The string attribute is optionally cheked for a value in the frontmatter. -func (m otherInt64Changed) PlanModifyInt64( - ctx context.Context, - req planmodifier.Int64Request, - resp *planmodifier.Int64Response, -) { - m.req = req - m.resp = resp - m.modifyAttribute(ctx) -} - -// PlanModifyBool implements a modifier for planning a change for a *bool* -// attribute if another specified *int64* attribute changes. -func (m otherInt64Changed) PlanModifyBool( - ctx context.Context, - req planmodifier.BoolRequest, - resp *planmodifier.BoolResponse, -) { - m.req = req - m.resp = resp - m.modifyAttribute(ctx) -} diff --git a/readme/otherattributemodifier/string.go b/readme/otherattributemodifier/string.go deleted file mode 100644 index d92283c..0000000 --- a/readme/otherattributemodifier/string.go +++ /dev/null @@ -1,256 +0,0 @@ -package otherattributemodifier - -// Plan modifier for planning a change for an attribute if another specified -// *string* attribute changes. - -import ( - "context" - "fmt" - "reflect" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/liveoaklabs/terraform-provider-readme/readme/frontmatter" -) - -// otherStringChanged is a plan modifier that plans a change for an -// attribute if another specified *string* attribute is changed. -type otherStringChanged struct { - otherAttribute path.Path - otherField string - checkFrontmatter bool - req interface{} - resp interface{} -} - -// StringModifyString is a plan modifier to flag a *string* attribute -// for change if another specified *string* attribute changes. -// The attribute argument is the path to the attribute to check. -// The otherField argument is the name of the Go struct field in the -// frontmatter. -// The checkFrontmatter argument is a boolean to indicate whether to -// check the frontmatter if the attribute is not set. -func StringModifyString( - attribute path.Path, - otherField string, - checkFrontmatter bool, -) planmodifier.String { - return otherStringChanged{ - otherAttribute: attribute, - otherField: otherField, - checkFrontmatter: checkFrontmatter, - } -} - -// Int64ModifyString is a plan modifier to flag an *int64* attribute for -// change if another specified *string* attribute changes. -// The attribute argument is the path to the attribute to check. -// The otherField argument is the name of the Go struct field in the -// frontmatter. -// The checkFrontmatter argument is a boolean to indicate whether to -// check the frontmatter if the attribute is not set. -func StringModifyInt64( - attribute path.Path, - otherField string, - checkFrontmatter bool, -) planmodifier.Int64 { - return otherStringChanged{ - otherAttribute: attribute, - otherField: otherField, - checkFrontmatter: checkFrontmatter, - } -} - -// Description returns a plain text description of the modifier's behavior. -func (m otherStringChanged) Description(ctx context.Context) string { - return "If another attribute is changed, this attribute will be changed." -} - -// MarkdownDescription returns a markdown formatted description of the -// modifier's behavior. -func (m otherStringChanged) MarkdownDescription(ctx context.Context) string { - return m.Description(ctx) -} - -// Load the config, plan, state, and body plan values based on the type. -// The config is loaded to know whether to check frontmatter if the -// attribute is not set. Load the plan and state to compare across values. -// The body plan value is loaded to check frontmatter if the attribute is not set. -func (m otherStringChanged) loadValues( - ctx context.Context, -) (types.String, types.String, types.String, types.String, diag.Diagnostics) { - var bodyValue, configValue, planValue, stateValue types.String - var diags diag.Diagnostics - - switch m.req.(type) { - case planmodifier.StringRequest: - req := m.req.(planmodifier.StringRequest) - resp := m.resp.(*planmodifier.StringResponse) - rDiag := resp.Diagnostics - - rDiag.Append(req.Config.GetAttribute(ctx, m.otherAttribute, &configValue)...) - rDiag.Append(req.State.GetAttribute(ctx, m.otherAttribute, &stateValue)...) - rDiag.Append(req.Plan.GetAttribute(ctx, m.otherAttribute, &planValue)...) - rDiag.Append(req.Plan.GetAttribute(ctx, path.Root("body"), &bodyValue)...) - case planmodifier.Int64Request: - req := m.req.(planmodifier.Int64Request) - resp := m.resp.(*planmodifier.Int64Response) - rDiag := resp.Diagnostics - - rDiag.Append(req.State.GetAttribute(ctx, m.otherAttribute, &stateValue)...) - rDiag.Append(req.Config.GetAttribute(ctx, m.otherAttribute, &configValue)...) - rDiag.Append(req.Plan.GetAttribute(ctx, m.otherAttribute, &planValue)...) - rDiag.Append(req.Plan.GetAttribute(ctx, path.Root("body"), &bodyValue)...) - case planmodifier.BoolRequest: - req := m.req.(planmodifier.BoolRequest) - resp := m.resp.(*planmodifier.BoolResponse) - rDiag := resp.Diagnostics - - rDiag.Append(req.State.GetAttribute(ctx, m.otherAttribute, &stateValue)...) - rDiag.Append(req.Config.GetAttribute(ctx, m.otherAttribute, &configValue)...) - rDiag.Append(req.Plan.GetAttribute(ctx, m.otherAttribute, &planValue)...) - rDiag.Append(req.Plan.GetAttribute(ctx, path.Root("body"), &bodyValue)...) - } - - return configValue, stateValue, planValue, bodyValue, diags -} - -// modifyAttribute is a helper function to modify the attribute based on the -// type of the request and response. -// -// The repetition is necessary because the request and response types are -// different for each type. -// func (m *otherStringChanged) modifyAttribute(ctx context.Context, resp interface{}) { -func (m *otherStringChanged) modifyAttribute(ctx context.Context) { - var isChanged bool - var diags diag.Diagnostics - - // Load the config, plan, state, and body plan values based on the type. - otherConfigValue, otherStateValue, otherPlanValue, bodyPlanValue, diags := m.loadValues(ctx) - - // If the attribute isn't set, check the body front matter. - if m.checkFrontmatter && otherConfigValue.IsNull() { - value, errStr := frontmatter.GetValue(ctx, bodyPlanValue.ValueString(), m.otherField) - if errStr != "" { - diags.AddError("Error parsing front matter.", errStr) - - return - } - - // If the value from frontmatter is not empty, compare it to the current state. - if value != (reflect.Value{}) { - tflog.Debug(ctx, fmt.Sprintf( - "%s was found in frontmatter with value %s", - m.otherAttribute, value)) - - // If the value from frontmatter is different from the current - // plan, mark this attribute as changed. - isChanged = value.Interface().(string) != otherPlanValue.ValueString() - } else { - tflog.Debug(ctx, fmt.Sprintf( - "value for %s was not found in frontmatter", - m.otherAttribute)) - } - } else { - // If the attribute is set, compare it to the current state and ignore - // the frontmatter. - tflog.Debug(ctx, "otherStringChanged: not checking front matter") - isChanged = otherConfigValue != otherStateValue && !otherStateValue.IsNull() - } - - tflog.Debug(ctx, fmt.Sprintf( - "otherStringChanged: %s otherConfigValue (%s) otherStateValue (%s) isChanged (%v)", - m.otherAttribute, otherConfigValue, otherStateValue, isChanged)) - - // If the other attribute is changed, set this attribute unknown to trigger - // an update. Otherwise, set it to the current plan value. - switch m.req.(type) { - case planmodifier.StringRequest: - resp := m.resp.(*planmodifier.StringResponse) - req := m.req.(planmodifier.StringRequest) - - if isChanged { - tflog.Debug(ctx, fmt.Sprintf( - "%s: setting value to unknown", req.Path)) - - resp.PlanValue = types.StringUnknown() - - return - } - - resp.PlanValue = req.PlanValue - case planmodifier.Int64Request: - resp := m.resp.(*planmodifier.Int64Response) - req := m.req.(planmodifier.Int64Request) - - if isChanged { - tflog.Debug(ctx, fmt.Sprintf( - "%s: setting value to unknown", req.Path)) - - resp.PlanValue = types.Int64Unknown() - - return - } - - tflog.Debug(ctx, fmt.Sprintf( - "%s: setting value from plan", req.Path)) - - resp.PlanValue = req.PlanValue - case planmodifier.BoolRequest: - resp := m.resp.(*planmodifier.BoolResponse) - req := m.req.(planmodifier.BoolRequest) - - if isChanged { - tflog.Debug(ctx, fmt.Sprintf( - "%s: setting value to unknown", req.Path)) - resp.PlanValue = types.BoolUnknown() - - return - } - - resp.PlanValue = req.PlanValue - default: - tflog.Info(ctx, fmt.Sprintf( - "otherStringChanged: unknown request type %T", m.req)) - } -} - -// PlanModifyString implements a modifier for planning a change for an -// attribute if another specified *string* attribute changes. -func (m otherStringChanged) PlanModifyString( - ctx context.Context, - req planmodifier.StringRequest, - resp *planmodifier.StringResponse, -) { - m.req = req - m.resp = resp - m.modifyAttribute(ctx) -} - -// PlanModifyInt64 implements a modifier for planning a change for an -// *int64* attribute if another specified *string* attribute changes. -// The string attribute is optionally cheked for a value in the frontmatter. -func (m otherStringChanged) PlanModifyInt64( - ctx context.Context, - req planmodifier.Int64Request, - resp *planmodifier.Int64Response, -) { - m.req = req - m.resp = resp - m.modifyAttribute(ctx) -} - -// PlanModifyBool implements a modifier for planning a change for a *bool* -// attribute if another specified *string* attribute changes. -func (m otherStringChanged) PlanModifyBool( - ctx context.Context, - req planmodifier.BoolRequest, - resp *planmodifier.BoolResponse, -) { - m.req = req - m.resp = resp - m.modifyAttribute(ctx) -} diff --git a/readme/version_resource.go b/readme/version_resource.go index 6e16e84..a32c689 100644 --- a/readme/version_resource.go +++ b/readme/version_resource.go @@ -16,7 +16,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/liveoaklabs/readme-api-go-client/readme" - "github.com/liveoaklabs/terraform-provider-readme/readme/otherattributemodifier" ) // Ensure the implementation satisfies the expected interfaces. @@ -233,14 +232,30 @@ func (r *versionResource) Schema( "version_clean": schema.StringAttribute{ Description: "A 'clean' version string with certain characters replaced, usually a semantic version.", Computed: true, - PlanModifiers: []planmodifier.String{ - otherattributemodifier.StringModifyString(path.Root("version"), "Version", false), - }, }, }, } } +func (r *versionResource) ModifyPlan( + ctx context.Context, + req resource.ModifyPlanRequest, + resp *resource.ModifyPlanResponse, +) { + var plan, state *versionResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + + if state == nil || plan == nil { + return + } + + // If the 'version' changes, update the 'version_clean' attribute. + if !plan.Version.Equal(state.Version) { + plan.VersionClean = types.StringUnknown() + } +} + // Create a version and set the initial Terraform state. func (r *versionResource) Create( ctx context.Context,