Skip to content

Commit

Permalink
Refactor and expose ProviderSchemaFromJson
Browse files Browse the repository at this point in the history
  • Loading branch information
radeksimko committed Apr 16, 2021
1 parent 334bd44 commit 68b8f6b
Show file tree
Hide file tree
Showing 9 changed files with 418 additions and 300 deletions.
6 changes: 6 additions & 0 deletions earlydecoder/decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ resource "google_storage_bucket" "bucket" {
name = "test-bucket"
}
data "blah_foobar" "test" {
name = "something"
}
provider "grafana" {
url = "http://grafana.example.com/"
org_id = 1
Expand All @@ -62,11 +66,13 @@ provider "grafana" {
Path: path,
ProviderReferences: map[module.ProviderRef]tfaddr.Provider{
{LocalName: "aws"}: tfaddr.NewLegacyProvider("aws"),
{LocalName: "blah"}: tfaddr.NewLegacyProvider("blah"),
{LocalName: "google"}: tfaddr.NewLegacyProvider("google"),
{LocalName: "grafana"}: tfaddr.NewLegacyProvider("grafana"),
},
ProviderRequirements: map[tfaddr.Provider]version.Constraints{
tfaddr.NewLegacyProvider("aws"): {},
tfaddr.NewLegacyProvider("blah"): {},
tfaddr.NewLegacyProvider("google"): {},
tfaddr.NewLegacyProvider("grafana"): {},
},
Expand Down
238 changes: 238 additions & 0 deletions schema/convert_json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
package schema

import (
"fmt"

"github.com/hashicorp/go-version"
"github.com/hashicorp/hcl-lang/lang"
"github.com/hashicorp/hcl-lang/schema"
"github.com/hashicorp/terraform-json"
"github.com/hashicorp/terraform-registry-address"
"github.com/zclconf/go-cty/cty"
)

func ProviderSchemaFromJson(jsonSchema *tfjson.ProviderSchema, pAddr tfaddr.Provider) *ProviderSchema {
ps := &ProviderSchema{
Resources: map[string]*schema.BodySchema{},
DataSources: map[string]*schema.BodySchema{},
}

if jsonSchema.ConfigSchema != nil {
ps.Provider = bodySchemaFromJson(jsonSchema.ConfigSchema.Block)
ps.Provider.Detail = detailForSrcAddr(pAddr, nil)
ps.Provider.DocsLink = docsLinkForProvider(pAddr, nil)
}

for rName, rSchema := range jsonSchema.ResourceSchemas {
ps.Resources[rName] = bodySchemaFromJson(rSchema.Block)
ps.Resources[rName].Detail = detailForSrcAddr(pAddr, nil)
}

for dsName, dsSchema := range jsonSchema.DataSourceSchemas {
ps.DataSources[dsName] = bodySchemaFromJson(dsSchema.Block)
ps.DataSources[dsName].Detail = detailForSrcAddr(pAddr, nil)
}

return ps
}

func (ps *ProviderSchema) SetProviderVersion(pAddr tfaddr.Provider, v *version.Version) {
if ps.Provider != nil {
ps.Provider.Detail = detailForSrcAddr(pAddr, v)
ps.Provider.DocsLink = docsLinkForProvider(pAddr, v)
}
for _, rSchema := range ps.Resources {
rSchema.Detail = detailForSrcAddr(pAddr, v)
}
for _, dsSchema := range ps.DataSources {
dsSchema.Detail = detailForSrcAddr(pAddr, v)
}
}

func bodySchemaFromJson(schemaBlock *tfjson.SchemaBlock) *schema.BodySchema {
if schemaBlock == nil {
s := schema.NewBodySchema()
return s
}

return &schema.BodySchema{
Attributes: convertAttributesFromJson(schemaBlock.Attributes),
Blocks: convertBlocksFromJson(schemaBlock.NestedBlocks),
IsDeprecated: schemaBlock.Deprecated,
Description: markupContent(schemaBlock.Description, schemaBlock.DescriptionKind),
}
}

func convertBlocksFromJson(blocks map[string]*tfjson.SchemaBlockType) map[string]*schema.BlockSchema {
cBlocks := make(map[string]*schema.BlockSchema, len(blocks))
for name, jsonSchema := range blocks {
block := jsonSchema.Block

blockType := schema.BlockTypeNil
labels := []*schema.LabelSchema{}

switch jsonSchema.NestingMode {
case tfjson.SchemaNestingModeSingle:
blockType = schema.BlockTypeObject
case tfjson.SchemaNestingModeMap:
labels = []*schema.LabelSchema{
{Name: "name"},
}
blockType = schema.BlockTypeMap
case tfjson.SchemaNestingModeList:
blockType = schema.BlockTypeList
case tfjson.SchemaNestingModeSet:
blockType = schema.BlockTypeSet
}

cBlocks[name] = &schema.BlockSchema{
Description: markupContent(block.Description, block.DescriptionKind),
Type: blockType,
IsDeprecated: block.Deprecated,
MinItems: jsonSchema.MinItems,
MaxItems: jsonSchema.MaxItems,
Labels: labels,
Body: bodySchemaFromJson(block),
}
}
return cBlocks
}

func convertAttributesFromJson(attributes map[string]*tfjson.SchemaAttribute) map[string]*schema.AttributeSchema {
cAttrs := make(map[string]*schema.AttributeSchema, len(attributes))
for name, attr := range attributes {
cAttrs[name] = &schema.AttributeSchema{
Description: markupContent(attr.Description, attr.DescriptionKind),
IsDeprecated: attr.Deprecated,
IsComputed: attr.Computed,
IsOptional: attr.Optional,
IsRequired: attr.Required,
Expr: exprConstraintsFromAttribute(attr),
}
}
return cAttrs
}

func exprConstraintsFromAttribute(attr *tfjson.SchemaAttribute) schema.ExprConstraints {
var expr schema.ExprConstraints
if attr.AttributeType != cty.NilType {
expr = schema.LiteralTypeOnly(attr.AttributeType)
}
if attr.AttributeNestedType != nil {
switch attr.AttributeNestedType.NestingMode {
case tfjson.SchemaNestingModeSingle:
return schema.ExprConstraints{
convertJsonAttributesToObjectExprAttr(attr.AttributeNestedType.Attributes),
}
case tfjson.SchemaNestingModeList:
return schema.ExprConstraints{
schema.ListExpr{
Elem: schema.ExprConstraints{
convertJsonAttributesToObjectExprAttr(attr.AttributeNestedType.Attributes),
},
MinItems: attr.AttributeNestedType.MinItems,
MaxItems: attr.AttributeNestedType.MaxItems,
},
}
case tfjson.SchemaNestingModeSet:
return schema.ExprConstraints{
schema.SetExpr{
Elem: schema.ExprConstraints{
convertJsonAttributesToObjectExprAttr(attr.AttributeNestedType.Attributes),
},
MinItems: attr.AttributeNestedType.MinItems,
MaxItems: attr.AttributeNestedType.MaxItems,
},
}
case tfjson.SchemaNestingModeMap:
return schema.ExprConstraints{
schema.MapExpr{
Elem: schema.ExprConstraints{
convertJsonAttributesToObjectExprAttr(attr.AttributeNestedType.Attributes),
},
MinItems: attr.AttributeNestedType.MinItems,
MaxItems: attr.AttributeNestedType.MaxItems,
},
}
}
}
return expr
}

func convertJsonAttributesToObjectExprAttr(attrs map[string]*tfjson.SchemaAttribute) schema.ObjectExpr {
attributes := make(schema.ObjectExprAttributes, len(attrs))
for name, attr := range attrs {
attributes[name] = &schema.AttributeSchema{
Description: markupContent(attr.Description, attr.DescriptionKind),
IsDeprecated: attr.Deprecated,
IsComputed: attr.Computed,
IsOptional: attr.Optional,
IsRequired: attr.Required,
Expr: exprConstraintsFromAttribute(attr),
}
}
return schema.ObjectExpr{
Attributes: attributes,
}
}

func markupContent(value string, kind tfjson.SchemaDescriptionKind) lang.MarkupContent {
if value == "" {
return lang.MarkupContent{}
}
switch kind {
case tfjson.SchemaDescriptionKindMarkdown:
return lang.Markdown(value)
case tfjson.SchemaDescriptionKindPlain:
return lang.PlainText(value)
}

// backwards compatibility with v0.12
return lang.PlainText(value)
}

func docsLinkForProvider(addr tfaddr.Provider, v *version.Version) *schema.DocsLink {
if !providerHasDocs(addr) {
return nil
}

ver := "latest"
if v != nil {
ver = v.String()
}

return &schema.DocsLink{
URL: fmt.Sprintf("https://registry.terraform.io/providers/%s/%s/%s/docs",
addr.Namespace, addr.Type, ver),
Tooltip: fmt.Sprintf("%s Documentation", addr.ForDisplay()),
}
}

func providerHasDocs(addr tfaddr.Provider) bool {
if addr.IsBuiltIn() {
// Ideally this should point to versioned TF core docs
// but there aren't any for the built-in provider yet
return false
}
if addr.Hostname != "registry.terraform.io" {
// docs URLs outside of the official Registry aren't standardized yet
return false
}
return true
}

func detailForSrcAddr(addr tfaddr.Provider, v *version.Version) string {
if addr.IsBuiltIn() {
if v == nil {
return "(builtin)"
}
return fmt.Sprintf("(builtin %s)", v.String())
}

detail := addr.ForDisplay()
if v != nil {
detail += " " + v.String()
}

return detail
}
82 changes: 82 additions & 0 deletions schema/convert_json_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package schema

import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/go-version"
"github.com/hashicorp/hcl-lang/schema"
tfaddr "github.com/hashicorp/terraform-registry-address"
"github.com/zclconf/go-cty-debug/ctydebug"
"github.com/zclconf/go-cty/cty"
)

func TestProviderSchema_SetProviderVersion(t *testing.T) {
ps := &ProviderSchema{
Provider: &schema.BodySchema{},
Resources: map[string]*schema.BodySchema{
"foo": {
Attributes: map[string]*schema.AttributeSchema{
"str": {
Expr: schema.LiteralTypeOnly(cty.String),
IsOptional: true,
},
},
},
},
DataSources: map[string]*schema.BodySchema{
"bar": {
Attributes: map[string]*schema.AttributeSchema{
"num": {
Expr: schema.LiteralTypeOnly(cty.Number),
IsOptional: true,
},
},
},
},
}
expectedSchema := &ProviderSchema{
Provider: &schema.BodySchema{
Detail: "hashicorp/aws 1.2.5",
DocsLink: &schema.DocsLink{
URL: "https://registry.terraform.io/providers/hashicorp/aws/1.2.5/docs",
Tooltip: "hashicorp/aws Documentation",
},
},
Resources: map[string]*schema.BodySchema{
"foo": {
Detail: "hashicorp/aws 1.2.5",
Attributes: map[string]*schema.AttributeSchema{
"str": {
Expr: schema.LiteralTypeOnly(cty.String),
IsOptional: true,
},
},
},
},
DataSources: map[string]*schema.BodySchema{
"bar": {
Detail: "hashicorp/aws 1.2.5",
Attributes: map[string]*schema.AttributeSchema{
"num": {
Expr: schema.LiteralTypeOnly(cty.Number),
IsOptional: true,
},
},
},
},
}

pAddr := tfaddr.Provider{
Hostname: tfaddr.DefaultRegistryHost,
Namespace: "hashicorp",
Type: "aws",
}
pv := version.Must(version.NewVersion("1.2.5"))

ps.SetProviderVersion(pAddr, pv)

if diff := cmp.Diff(expectedSchema, ps, ctydebug.CmpOptions); diff != "" {
t.Fatalf("unexpected schema: %s", diff)
}
}
Loading

0 comments on commit 68b8f6b

Please sign in to comment.