Skip to content

Commit f89067e

Browse files
authored
Add Provider Schema support with new schema_ref config option (#40)
* move name property to mapper * add validation and config * add schema proxy to explorer * implemented basics of provider + fixed resolving bug with nested refs * update library dep * cleanup from merged PRs * switch collection to use validators * add map validators * upgrade and convert float values * move function * add docs * copywrite!
1 parent 244dff4 commit f89067e

30 files changed

Lines changed: 2300 additions & 36 deletions

DESIGN.md

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,38 @@ Users of the generator can adjust their OAS to match these assumptions, or sugge
1919
## Determining the OAS Schema to map from operations
2020

2121
### Provider
22-
<!-- TODO: Update this when we have the provider schema mapping :) -->
23-
Currently, there is no option in the OpenAPI generator to define the mapping for a provider schema. The `provider.name` property is directly copied to the Framework IR.
22+
For generating Provider schema code, the [generator config file](./README.md) defines:
23+
- `provider.name` - required property, which is directly copied to the Framework IR as the name of the Provider.
24+
- `provider.schema_ref` - optional property, which is a [JSON schema reference](https://json-schema.org/understanding-json-schema/structuring.html#ref) to an existing schema in your OpenAPI spec, typically in the [`components.schema` section](https://spec.openapis.org/oas/v3.1.0#fixed-fields-5). This will be used to [map](#mapping-oas-schema-to-plugin-framework-types) the Provider's schema to Framework IR.
25+
```yml
26+
provider:
27+
name: fakeprovider
28+
# This schema needs to exist in the OpenAPI spec!
29+
schema_ref: '#/components/schemas/fake_provider_schema'
30+
```
2431
2532
### Resources
2633
The [generator config file](./README.md) defines the CRUD (`Create`, `Read`, `Update`, `Delete`) operations for a resource in an OAS. In those operations, the generator will search `Create` and `Read` operations for schemas to map to Framework IR. Multiple schemas will be [deep merged](#deep-merge-of-schemas-resources) and the final result will be the Resource schema represented in Framework IR.
2734

35+
```yml
36+
resources:
37+
fake_thing:
38+
# Required
39+
create:
40+
path: /thing
41+
method: POST
42+
read:
43+
path: /thing/{id}
44+
method: GET
45+
# Optional (currently, no effect)
46+
update:
47+
path: /thing
48+
method: PUT
49+
delete:
50+
path: /thing/{id}
51+
method: DELETE
52+
```
53+
2854
#### OAS Schema order (resources)
2955
- `Create` operation [requestBody](https://spec.openapis.org/oas/v3.1.0#requestBodyObject)
3056
- `requestBody` is the only schema **required** for resources, if not present will log a warning and skip the resource without mapping.
@@ -48,6 +74,14 @@ All schemas found will be deep merged together, with the `requestBody` schema fr
4874
### Data Sources
4975
The [generator config file](./README.md) defines the `Read` operation for a data source in an OAS. In that operation, the generator will search for a response body schema to map to Framework IR. The response body will be [deep merged](#deep-merge-of-schemas-data-sources) with the query parameters and path parameters of the same `Read` operation and the final result will be the Data Source schema represented in Framework IR.
5076

77+
```yml
78+
data_sources:
79+
fake_thing:
80+
read:
81+
path: /thing/{id}
82+
method: GET
83+
```
84+
5185
#### OAS Schema order (data sources)
5286
- `Read` operation [response](https://spec.openapis.org/oas/v3.1.0#responsesObject)
5387
- `response` is the only schema **required** for data sources, if not present will log a warning and skip the data source without mapping.
@@ -110,6 +144,11 @@ For attributes that don't have additional schema information (`ListAttribute`, `
110144

111145
### Required, Computed, and Optional
112146

147+
#### Provider
148+
For the provider, all fields in the provided JSON schema (`provider.schema_ref`) marked as [required](https://json-schema.org/understanding-json-schema/reference/object.html#required-properties) will be mapped as a [Required](https://developer.hashicorp.com/terraform/plugin/framework/handling-data/schemas#required) attribute.
149+
150+
If not required, then the field will be mapped as [Optional](https://developer.hashicorp.com/terraform/plugin/framework/handling-data/schemas#optional).
151+
113152
#### Resources
114153
For resources, all fields, in the `Create` operation `requestBody` OAS schema, marked as [required](https://json-schema.org/understanding-json-schema/reference/object.html#required-properties) will be mapped as a [Required](https://developer.hashicorp.com/terraform/plugin/framework/handling-data/schemas#required) attribute.
115154

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ require (
77
github.com/hashicorp/terraform-plugin-codegen-spec v0.0.0-20230623193314-2a16a9d0b6f3
88
github.com/mattn/go-colorable v0.1.13
99
github.com/mitchellh/cli v1.1.5
10-
github.com/pb33f/libopenapi v0.8.5
10+
github.com/pb33f/libopenapi v0.9.2
1111
gopkg.in/yaml.v3 v3.0.1
1212
)
1313

@@ -35,6 +35,6 @@ require (
3535
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
3636
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
3737
golang.org/x/crypto v0.7.0 // indirect
38-
golang.org/x/sync v0.1.0 // indirect
38+
golang.org/x/sync v0.3.0 // indirect
3939
golang.org/x/sys v0.7.0 // indirect
4040
)

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
101101
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
102102
github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
103103
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
104-
github.com/pb33f/libopenapi v0.8.5 h1:9wOa/qu7mvuX8rgi/VlrKycS/Y1EaeIZaq3iILitgPI=
105-
github.com/pb33f/libopenapi v0.8.5/go.mod h1:lvUmCtjgHUGVj6WzN3I5/CS9wkXtyN3Ykjh6ZZP5lrI=
104+
github.com/pb33f/libopenapi v0.9.2 h1:QFxTgTSmW9mnXhQ+myignBh19ZPkFRxnAvbPnFcesDs=
105+
github.com/pb33f/libopenapi v0.9.2/go.mod h1:lvUmCtjgHUGVj6WzN3I5/CS9wkXtyN3Ykjh6ZZP5lrI=
106106
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
107107
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
108108
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
@@ -160,8 +160,8 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
160160
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
161161
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
162162
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
163-
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
164-
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
163+
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
164+
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
165165
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
166166
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
167167
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

internal/cmd/generate.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
"github.com/hashicorp/terraform-plugin-codegen-openapi/internal/config"
1616
"github.com/hashicorp/terraform-plugin-codegen-openapi/internal/explorer"
1717
"github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper"
18-
"github.com/hashicorp/terraform-plugin-codegen-spec/provider"
1918
"github.com/hashicorp/terraform-plugin-codegen-spec/spec"
2019

2120
"github.com/mitchellh/cli"
@@ -198,10 +197,15 @@ func generateFrameworkIr(dora explorer.Explorer, cfg config.Config) (*spec.Speci
198197
return nil, fmt.Errorf("error generating Framework IR for data sources: %w", err)
199198
}
200199

200+
// 6. Use TF info to generate framework IR for provider
201+
providerMapper := mapper.NewProviderMapper(explorerProvider, cfg)
202+
providerIR, err := providerMapper.MapToIR()
203+
if err != nil {
204+
return nil, fmt.Errorf("error generating Framework IR for provider: %w", err)
205+
}
206+
201207
return &spec.Specification{
202-
Provider: &provider.Provider{
203-
Name: explorerProvider.Name,
204-
},
208+
Provider: providerIR,
205209
Resources: resourcesIR,
206210
DataSources: dataSourcesIR,
207211
}, nil

internal/cmd/testdata/edgecase/generated_framework_ir.json

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,54 @@
164164
}
165165
],
166166
"provider": {
167-
"name": "edgecase"
167+
"name": "edgecase",
168+
"schema": {
169+
"attributes": [
170+
{
171+
"name": "bool_prop",
172+
"bool": {
173+
"optional_required": "optional",
174+
"description": "Bool for the provider"
175+
}
176+
},
177+
{
178+
"name": "string_prop",
179+
"string": {
180+
"optional_required": "required",
181+
"description": "String for the provider"
182+
}
183+
},
184+
{
185+
"name": "triple_nested_map",
186+
"list": {
187+
"optional_required": "required",
188+
"element_type": {
189+
"set": {
190+
"element_type": {
191+
"map": {
192+
"element_type": {
193+
"object": {
194+
"attribute_types": [
195+
{
196+
"name": "bool_prop",
197+
"bool": {}
198+
},
199+
{
200+
"name": "string_prop",
201+
"string": {}
202+
}
203+
]
204+
}
205+
}
206+
}
207+
}
208+
}
209+
},
210+
"description": "This list has a set of maps nested underneath!"
211+
}
212+
}
213+
]
214+
}
168215
},
169216
"resources": [
170217
{

internal/cmd/testdata/edgecase/openapi_spec.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,21 @@ paths:
100100
type: string
101101
components:
102102
schemas:
103+
edgecase_provider:
104+
description: This is the provider schema
105+
type: object
106+
required:
107+
- string_prop
108+
- triple_nested_map
109+
properties:
110+
string_prop:
111+
description: String for the provider
112+
type: string
113+
bool_prop:
114+
description: Bool for the provider
115+
type: boolean
116+
triple_nested_map:
117+
$ref: "#/components/schemas/triple_nested_map_schema"
103118
double_nested_list_schema:
104119
description: This list has a list nested underneath!
105120
type: array

internal/cmd/testdata/edgecase/tfopenapigen_config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
provider:
22
name: edgecase
3+
schema_ref: '#/components/schemas/edgecase_provider'
34

45
resources:
56
set_test:

internal/config/parse.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"errors"
88
"fmt"
99

10+
"github.com/pb33f/libopenapi/index"
1011
"gopkg.in/yaml.v3"
1112
)
1213

@@ -18,7 +19,8 @@ type Config struct {
1819
}
1920

2021
type Provider struct {
21-
Name string `yaml:"name"`
22+
Name string `yaml:"name"`
23+
SchemaRef string `yaml:"schema_ref"`
2224
}
2325

2426
type Resource struct {
@@ -90,6 +92,11 @@ func (p Provider) Validate() error {
9092
return errors.New("must have a 'name' property")
9193
}
9294

95+
// All schema refs must be a local, file, or http resolve type
96+
if p.SchemaRef != "" && index.DetermineReferenceResolveType(p.SchemaRef) < 0 {
97+
return errors.New("'schema_ref' must be a valid JSON schema reference")
98+
}
99+
93100
return nil
94101
}
95102

internal/config/parse_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,19 @@ func TestParseConfig_Invalid(t *testing.T) {
106106
input: ``,
107107
expectedErrRegex: `provider must have a 'name' property`,
108108
},
109+
"provider - invalid schema_ref - not resolvable": {
110+
input: `
111+
provider:
112+
name: example
113+
schema_ref: thisaintvalid
114+
115+
data_sources:
116+
thing_one:
117+
read:
118+
path: /example/path/to/thing/{id}
119+
method: GET`,
120+
expectedErrRegex: `provider 'schema_ref' must be a valid JSON schema reference`,
121+
},
109122
"at least one resource or data source required": {
110123
input: `
111124
provider:

internal/explorer/config_explorer.go

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@
44
package explorer
55

66
import (
7+
"fmt"
78
"strings"
89

910
"github.com/hashicorp/terraform-plugin-codegen-openapi/internal/config"
1011

12+
highbase "github.com/pb33f/libopenapi/datamodel/high/base"
1113
high "github.com/pb33f/libopenapi/datamodel/high/v3"
14+
lowmodel "github.com/pb33f/libopenapi/datamodel/low"
15+
lowbase "github.com/pb33f/libopenapi/datamodel/low/base"
1216
low "github.com/pb33f/libopenapi/datamodel/low/v3"
1317
)
1418

@@ -31,9 +35,21 @@ func NewConfigExplorer(spec high.Document, cfg config.Config) Explorer {
3135
}
3236

3337
func (e configExplorer) FindProvider() (Provider, error) {
34-
return Provider{
38+
foundProvider := Provider{
3539
Name: e.config.Provider.Name,
36-
}, nil
40+
}
41+
42+
if e.config.Provider.SchemaRef == "" {
43+
return foundProvider, nil
44+
}
45+
46+
schemaProxy, err := extractSchemaProxy(e.spec, e.config.Provider.SchemaRef)
47+
if err != nil {
48+
return Provider{}, fmt.Errorf("error extracting provider schema from ref: %w", err)
49+
}
50+
foundProvider.SchemaProxy = schemaProxy
51+
52+
return foundProvider, nil
3753
}
3854

3955
func (e configExplorer) FindResources() (map[string]Resource, error) {
@@ -87,3 +103,30 @@ func extractOp(paths *high.Paths, oasLocation *config.OpenApiSpecLocation) *high
87103
return nil
88104
}
89105
}
106+
107+
func extractSchemaProxy(document high.Document, componentRef string) (*highbase.SchemaProxy, error) {
108+
// find the reference using the root document.Index
109+
indexRef := document.Index.FindComponentInRoot(componentRef)
110+
if indexRef == nil {
111+
return nil, fmt.Errorf("unable to find reference: %s", componentRef)
112+
}
113+
114+
// build low-level schema using YAML node
115+
var lowSchema lowbase.Schema
116+
err := lowmodel.BuildModel(indexRef.Node, &lowSchema)
117+
if err != nil {
118+
return nil, fmt.Errorf("error building low-level schema: %w", err)
119+
}
120+
121+
// populate low-level schema, using root document.Index for resolving
122+
err = lowSchema.Build(indexRef.Node, document.Index)
123+
if err != nil {
124+
return nil, fmt.Errorf("error populating low-level schema: %w", err)
125+
}
126+
127+
// build high-level schema from low-level schema
128+
highSchema := highbase.NewSchema(&lowSchema)
129+
130+
// wrap in a schema proxy for mapping with `oas` package
131+
return highbase.CreateSchemaProxy(highSchema), nil
132+
}

0 commit comments

Comments
 (0)