Skip to content

Commit

Permalink
Merge pull request #88 from joshbeard/doc-use-slug
Browse files Browse the repository at this point in the history
feat: ability to associate doc with slug
  • Loading branch information
joshbeard authored Nov 28, 2023
2 parents 65fd9ee + 9a8e160 commit 380c7cd
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 24 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,12 @@ dist/

# Exclude test reports
checkstyle-report.xml
coverage.html
coverage.xml
coverage.txt

# Exclude local env files
.env.local

examples/data-sources/*/provider.tf
examples/resources/*/provider.tf
examples/resources/*/provider.tf
1 change: 1 addition & 0 deletions docs/data-sources/doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ output "example_doc" {
- `title` (String) The title of the doc.
- `type` (String) Type of the doc. The available types all show up under the /docs/ URL path of your docs project (also known as the "guides" section). Can be "basic" (most common), "error" (page desribing an API error), or "link" (page that redirects to an external link).
- `updated_at` (String) The timestamp of when the doc was last updated.
- `use_slug` (String) This is an unused attribute in the data source that is present to satisfy the model shared with the doc resource. It may be removed in the future.
- `user` (String) The ID of the author of the doc in the web editor.
- `version_id` (String) The version ID the doc is associated with.

Expand Down
29 changes: 27 additions & 2 deletions docs/resources/api_specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ description: |-
External changes made to an API specification managed by Terraform will not be detected due to the way the API registry works. When a specification definition is updated, the registry UUID changes and is only available from the response when the definition is published to the registry. When Terraform runs after an external update, there's no way of programatically retrieving the current state without the current UUID. Forcing a Terraform update (e.g. tainting or a manual change) will get things synchronized again.
Importing Existing Specifications
Importing API specifications is limited due to the behavior of the API registry and associating a specification with its definition. When importing, Terraform will replace the remote definition on its next run, regardless if it differs from the local definition. This will associate a registry UUID with the specification.
Managing API Specification Docs
API Specifications created in ReadMe can have a documentation page associated with them. This is automatically created by ReadMe when a specification is created. The documentation page is not implicitly managed by Terraform. To manage the documentation page, use the readme_doc resource with the use_slug attribute set to the API specification tag slug.
See https://docs.readme.com/main/reference/uploadapispecification for more information about this API endpoint.
---

Expand All @@ -26,6 +28,10 @@ External changes made to an API specification managed by Terraform will not be d

Importing API specifications is limited due to the behavior of the API registry and associating a specification with its definition. When importing, Terraform will replace the remote definition on its next run, regardless if it differs from the local definition. This will associate a registry UUID with the specification.

## Managing API Specification Docs

API Specifications created in ReadMe can have a documentation page associated with them. This is automatically created by ReadMe when a specification is created. The documentation page is not implicitly managed by Terraform. To manage the documentation page, use the `readme_doc` resource with the `use_slug` attribute set to the API specification tag slug.

See <https://docs.readme.com/main/reference/uploadapispecification> for more information about this API endpoint.

## Example Usage
Expand All @@ -48,8 +54,27 @@ output "created_spec_id" {
}
# Output the specification JSON of the created resource.
output "created_spec_json" {
value = readme_api_specification.example.definition
# output "created_spec_json" {
# value = readme_api_specification.example.definition
# }
# ---------------------------------------------------------------------------
# Example of associating a doc resource with the API specification's default
# doc that is automatically created.
resource "readme_doc" "example" {
# This will be the visible name of the API specification's default doc.
title = "store"
# Use the API specification's category ID.
category = readme_api_specification.example.category.id
# Specify the slug of the created API spec doc. This is the slug of a
# specification tag.
use_slug = "store"
order = 10
type = "basic"
body = "This is the Pet Store API specification's default doc."
}
```

Expand Down
1 change: 1 addition & 0 deletions docs/resources/doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ resource "readme_doc" "example" {
- `parent_doc_slug` (String) For a subpage, specify the parent doc slug instead of the ID.This attribute may be set in the body front matter with the `parentDocSlug` key.If a value isn't specified but `parent_doc` is, the provider will attempt to populate this value using the `parent_doc` ID unless `verify_parent_doc` is set to `false`.
- `title` (String) **Required.** The title of the doc.This attribute may optionally be set in the body front matter.
- `type` (String) **Required.** Type of the doc. The available types all show up under the /docs/ URL path of your docs project (also known as the "guides" section). Can be "basic" (most common), "error" (page desribing an API error), or "link" (page that redirects to an external link).This attribute may optionally be set in the body front matter.
- `use_slug` (String) **Use with caution!** Create the doc resource by importing an existing doc by its slug. This is non-conventional and should only be used when the slug is known and the doc is not managed by Terraform. This is useful for managing an API specification's doc that gets created automatically by ReadMe. When set, the specified doc will be replaced with the Terraform-managed doc. Changing the value will trigger a re-creation of the doc. If this is set and then unset, a new doc will be created but the existing doc will not be deleted. The existing doc will be orphaned and will not be managed by Terraform. If this is unset and then set, the existing doc will be deleted and the resource will be pointed to the specified doc. In the case of API specification docs, the doc is implicitly deleted when the API specification is deleted.
- `verify_parent_doc` (Boolean) Enables or disables the provider verifying the `parent_doc` exists. When using the `parent_doc` attribute with a hidden parent, the provider is unable to verify if the parent exists. Setting this to `false` will disable this behavior. When `false`, the `parent_doc_slug` value will not be resolved by the provider unless explicitly set. The `parent_doc_slug` attribute may be used as an alternative. Verifying a `parent_doc` by ID does not work if the parent is hidden.
- `version` (String) The version to create the doc under.

Expand Down
23 changes: 21 additions & 2 deletions examples/resources/readme_api_specification/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,25 @@ output "created_spec_id" {
}

# Output the specification JSON of the created resource.
output "created_spec_json" {
value = readme_api_specification.example.definition
# output "created_spec_json" {
# value = readme_api_specification.example.definition
# }

# ---------------------------------------------------------------------------
# Example of associating a doc resource with the API specification's default
# doc that is automatically created.
resource "readme_doc" "example" {
# This will be the visible name of the API specification's default doc.
title = "store"

# Use the API specification's category ID.
category = readme_api_specification.example.category.id

# Specify the slug of the created API spec doc. This is the slug of a
# specification tag.
use_slug = "store"

order = 10
type = "basic"
body = "This is the Pet Store API specification's default doc."
}
33 changes: 22 additions & 11 deletions readme/api_specification_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/liveoaklabs/readme-api-go-client/readme"
)

Expand Down Expand Up @@ -141,6 +142,11 @@ func (r *apiSpecificationResource) Schema(
"specification with its definition. When importing, Terraform will replace the remote definition on its " +
"next run, regardless if it differs from the local definition. This will associate a registry UUID " +
"with the specification.\n\n" +
"## Managing API Specification Docs\n\n" +
"API Specifications created in ReadMe can have a documentation page associated with them. This is " +
"automatically created by ReadMe when a specification is created. The documentation page is not " +
"implicitly managed by Terraform. To manage the documentation page, use the `readme_doc` resource " +
"with the `use_slug` attribute set to the API specification tag slug.\n\n" +
"See <https://docs.readme.com/main/reference/uploadapispecification> for more information about this API " +
"endpoint.",
Attributes: map[string]schema.Attribute{
Expand Down Expand Up @@ -234,7 +240,7 @@ func (r *apiSpecificationResource) Create(
}

// Create the specification.
plan, err := r.save("create", "", plan)
plan, err := r.save(ctx, saveActionCreate, "", plan)
if err != nil {
resp.Diagnostics.AddError("Unable to create API specification.", err.Error())

Expand Down Expand Up @@ -297,13 +303,15 @@ func (r *apiSpecificationResource) Read(

// Get the spec plan.
state, err := r.makePlan(
ctx,
state.ID.ValueString(),
currentDefinition,
state.UUID.ValueString(),
version,
)
if err != nil {
if strings.Contains(err.Error(), "API specification not found") {
tflog.Warn(ctx, fmt.Sprintf("API specification %s not found. Removing from state.", state.ID.ValueString()))
resp.State.RemoveResource(ctx)

return
Expand Down Expand Up @@ -349,7 +357,7 @@ func (r *apiSpecificationResource) Update(
}

// Create the specification.
plan, err := r.save("update", state.ID.ValueString(), plan)
plan, err := r.save(ctx, saveActionUpdate, state.ID.ValueString(), plan)
if err != nil {
resp.Diagnostics.AddError("Unable to update API specification.", err.Error())

Expand Down Expand Up @@ -429,7 +437,9 @@ func (r *apiSpecificationResource) ImportState(
// After creation or update, the specification is retrieved and `makePlan()` is called to map the results to the
// Terraform resource schema that is returned.
func (r *apiSpecificationResource) save(
action, specID string, plan apiSpecificationResourceModel,
ctx context.Context,
action saveAction,
specID string, plan apiSpecificationResourceModel,
) (apiSpecificationResourceModel, error) {
var registry readme.APIRegistrySaved
var response readme.APISpecificationSaved
Expand All @@ -449,7 +459,7 @@ func (r *apiSpecificationResource) save(

// Create or update an API specification associated with the API registry.
requestOptions := readme.RequestOptions{Version: version}
if action == "update" {
if action == saveActionUpdate {
response, apiResponse, err = r.client.APISpecification.Update(
specID,
UUIDPrefix+registry.RegistryUUID,
Expand Down Expand Up @@ -481,7 +491,7 @@ func (r *apiSpecificationResource) save(
deleteCategory := plan.DeleteCategory

// Get the spec plan.
plan, err = r.makePlan(response.ID, plan.Definition, registry.RegistryUUID, version)
plan, err = r.makePlan(ctx, response.ID, plan.Definition, registry.RegistryUUID, version)
if err != nil {
return apiSpecificationResourceModel{}, fmt.Errorf("unable to make plan: %+w", err)
}
Expand All @@ -495,9 +505,9 @@ func (r *apiSpecificationResource) save(
//
// If a version ID is provided instead of a semver, a call to the version API is
// made to determine the semver.
//
// `get()` is called to retrieve the remote specification that is mapped to the schema that is returned.
func (r *apiSpecificationResource) makePlan(
ctx context.Context,
specID string,
definition types.String,
registryUUID, version string,
Expand All @@ -512,7 +522,7 @@ func (r *apiSpecificationResource) makePlan(
}

// Retrieve metadata about the API specification.
spec, err := r.get(specID, version)
spec, err := r.get(ctx, specID, version)
if err != nil {
return apiSpecificationResourceModel{}, fmt.Errorf("error getting specification: %w", err)
}
Expand All @@ -534,12 +544,13 @@ func (r *apiSpecificationResource) makePlan(
}

// get is a helper function that retrieves a specification by ID and returns a readme.APISpecification struct.
func (r *apiSpecificationResource) get(specID, version string) (readme.APISpecification, error) {
func (r *apiSpecificationResource) get(ctx context.Context, specID, version string) (readme.APISpecification, error) {
requestOptions := readme.RequestOptions{Version: version}
specification, apiResponse, err := r.client.APISpecification.Get(specID, requestOptions)
specification, _, err := r.client.APISpecification.Get(specID, requestOptions)
if err != nil {
// return specification, errors.New(clientError(err, apiResponse))
return specification, fmt.Errorf("unable to get specification id %s: %s", specID, string(apiResponse.Body))
tflog.Error(ctx, fmt.Sprintf("Unable to get specification: %+v", err))

return specification, fmt.Errorf("unable to get specification id %s: %w", specID, err)
}

if specification.ID == "" {
Expand Down
2 changes: 2 additions & 0 deletions readme/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type docModel struct {
Type types.String `tfsdk:"type"`
UpdatedAt types.String `tfsdk:"updated_at"`
User types.String `tfsdk:"user"`
UseSlug types.String `tfsdk:"use_slug"`
VerifyParentDoc types.Bool `tfsdk:"verify_parent_doc"`
Version types.String `tfsdk:"version"`
VersionID types.String `tfsdk:"version_id"`
Expand Down Expand Up @@ -107,6 +108,7 @@ func docModelValue(ctx context.Context, doc readme.Doc, model docModel) docModel
Type: types.StringValue(doc.Type),
UpdatedAt: types.StringValue(doc.UpdatedAt),
User: types.StringValue(doc.User),
UseSlug: model.UseSlug,
VerifyParentDoc: model.VerifyParentDoc,
Version: model.Version,
VersionID: types.StringValue(doc.Version),
Expand Down
9 changes: 9 additions & 0 deletions readme/doc_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,15 @@ func (d *docDataSource) Schema(
Description: "The ID of the author of the doc in the web editor.",
Computed: true,
},
// This isn't used by the doc data source, but must be present because the struct
// is shared with the doc resource, which does use it.
// In the future, we may want to split the struct into separate types for the
// resource and data source.
"use_slug": schema.StringAttribute{
Description: "This is an unused attribute in the data source that is present to " +
"satisfy the model shared with the doc resource. It may be removed in the future.",
Computed: true,
},
"verify_parent_doc": schema.BoolAttribute{
Description: "Enables or disables the provider verifying the `parent_doc` exists. When using the " +
"`parent_doc` attribute with a hidden parent, the provider is unable to verify if the parent " +
Expand Down
Loading

0 comments on commit 380c7cd

Please sign in to comment.