Skip to content

Commit 900db91

Browse files
austinvallebflad
andauthored
Add configuration option to match parameters to root schema attributes (#47)
* update spec dependency * add filter for path and query params * initial implementation - no tests yet * add initial tests * add quick doc * add mapper tests * swap over to new config pattern * Update DESIGN.md Co-authored-by: Brian Flad <[email protected]> --------- Co-authored-by: Brian Flad <[email protected]>
1 parent ed05479 commit 900db91

17 files changed

+413
-71
lines changed

DESIGN.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,57 @@ If the field is in a different schema than the `Read` operation `parameters`, th
183183
| [pattern](https://json-schema.org/draft/2020-12/json-schema-validation.html#name-pattern) | [(StringAttribute).Validators](https://developer.hashicorp.com/terraform/plugin/framework/validation) |
184184
| [uniqueItems](https://json-schema.org/draft/2020-12/json-schema-validation.html#name-uniqueItems) | [(ListAttribute).Validators](https://developer.hashicorp.com/terraform/plugin/framework/validation) |
185185

186+
## Remapping OAS parameter naming to attribute naming
187+
188+
In an OpenAPI Specification, it's possible for path or query parameters to have slightly different names then it's associated attribute in a resource/data source schema. For example, `petId` (path parameter) and `id` (defined on Pet schema):
189+
190+
```json
191+
"paths": {
192+
"/pet/{petId}": {
193+
"get": {
194+
"tags": [
195+
"pet"
196+
],
197+
"summary": "Find pet by ID",
198+
"description": "Returns a single pet",
199+
"operationId": "getPetById",
200+
"parameters": [
201+
{
202+
"name": "petId",
203+
"in": "path",
204+
"description": "ID of pet to return",
205+
"required": true,
206+
"schema": {
207+
"type": "integer",
208+
"format": "int64"
209+
}
210+
}
211+
],
212+
...
213+
"components": {
214+
"schemas": {
215+
"Pet": {
216+
"properties": {
217+
"id": {
218+
"type": "integer",
219+
"format": "int64",
220+
"example": 10
221+
},
222+
```
223+
224+
You can merge `petId` with `Pet.id` in the resulting schema by defining the following in the [generator config file](./README.md):
225+
```yaml
226+
resources:
227+
pet:
228+
# create, read, update, delete configuration
229+
# ...
230+
schema:
231+
attributes:
232+
aliases:
233+
# Match 'petId' parameter to 'id'
234+
petId: id
235+
```
236+
186237
## Multi-type Support
187238

188239
Generally, [multi-types](https://cswr.github.io/JsonSchema/spec/multiple_types/) are not supported by the generator as the Terraform Plugin Framework does not support multi-types. There is one specific scenario that is supported by the generator and that is any type that is combined with the `null` type, as any Plugin Framework attribute can hold a [null](https://developer.hashicorp.com/terraform/plugin/framework/handling-data/attributes#null) type.

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.20
44

55
require (
66
github.com/google/go-cmp v0.5.9
7-
github.com/hashicorp/terraform-plugin-codegen-spec v0.0.0-20230623193314-2a16a9d0b6f3
7+
github.com/hashicorp/terraform-plugin-codegen-spec v0.0.0-20230804072831-c6dffebf6781
88
github.com/mattn/go-colorable v0.1.13
99
github.com/mitchellh/cli v1.1.5
1010
github.com/pb33f/libopenapi v0.9.2

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv
5454
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
5555
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
5656
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
57-
github.com/hashicorp/terraform-plugin-codegen-spec v0.0.0-20230623193314-2a16a9d0b6f3 h1:6IiMR/31Wu/HvEGhLNzx1wSWvYCU2gkRv39sB24Cpis=
58-
github.com/hashicorp/terraform-plugin-codegen-spec v0.0.0-20230623193314-2a16a9d0b6f3/go.mod h1:4aZFKiOI2xiQFOpeQMMyDL8vRmKAZXHUY4kWol7Fdco=
57+
github.com/hashicorp/terraform-plugin-codegen-spec v0.0.0-20230804072831-c6dffebf6781 h1:FvNFHIsSf/5Yd5DTsLXoMhKVx6pRdCdkwvEt/SmbUWg=
58+
github.com/hashicorp/terraform-plugin-codegen-spec v0.0.0-20230804072831-c6dffebf6781/go.mod h1:4aZFKiOI2xiQFOpeQMMyDL8vRmKAZXHUY4kWol7Fdco=
5959
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
6060
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
6161
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=

internal/cmd/testdata/github/generated_framework_ir.json

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
}
1313
},
1414
{
15-
"name": "repo",
15+
"name": "name",
1616
"string": {
1717
"computed_optional_required": "required",
1818
"description": "The name of the repository. The name is not case sensitive."
@@ -460,12 +460,6 @@
460460
"computed_optional_required": "computed"
461461
}
462462
},
463-
{
464-
"name": "name",
465-
"string": {
466-
"computed_optional_required": "computed"
467-
}
468-
},
469463
{
470464
"name": "network_count",
471465
"int64": {
@@ -11970,13 +11964,6 @@
1197011964
],
1197111965
"description": "A repository on GitHub."
1197211966
}
11973-
},
11974-
{
11975-
"name": "repo",
11976-
"string": {
11977-
"computed_optional_required": "computed_optional",
11978-
"description": "The name of the repository. The name is not case sensitive."
11979-
}
1198011967
}
1198111968
]
1198211969
}

internal/cmd/testdata/github/tfopenapigen_config.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,17 @@ resources:
1717
delete:
1818
path: /repos/{owner}/{repo}
1919
method: DELETE
20+
schema:
21+
attributes:
22+
aliases:
23+
repo: name
2024

2125
data_sources:
2226
repository:
2327
read:
2428
path: /repos/{owner}/{repo}
25-
method: GET
29+
method: GET
30+
schema:
31+
attributes:
32+
aliases:
33+
repo: name

internal/cmd/testdata/petstore3/generated_framework_ir.json

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"schema": {
66
"attributes": [
77
{
8-
"name": "orderId",
8+
"name": "id",
99
"int64": {
1010
"computed_optional_required": "required",
1111
"description": "ID of order that needs to be fetched"
@@ -17,12 +17,6 @@
1717
"computed_optional_required": "computed"
1818
}
1919
},
20-
{
21-
"name": "id",
22-
"int64": {
23-
"computed_optional_required": "computed"
24-
}
25-
},
2620
{
2721
"name": "petId",
2822
"int64": {
@@ -56,7 +50,7 @@
5650
"schema": {
5751
"attributes": [
5852
{
59-
"name": "petId",
53+
"name": "id",
6054
"int64": {
6155
"computed_optional_required": "required",
6256
"description": "ID of pet to return"
@@ -82,12 +76,6 @@
8276
]
8377
}
8478
},
85-
{
86-
"name": "id",
87-
"int64": {
88-
"computed_optional_required": "computed"
89-
}
90-
},
9179
{
9280
"name": "name",
9381
"string": {
@@ -218,13 +206,6 @@
218206
}
219207
]
220208
}
221-
},
222-
{
223-
"name": "orderId",
224-
"int64": {
225-
"computed_optional_required": "computed_optional",
226-
"description": "ID of order that needs to be fetched"
227-
}
228209
}
229210
]
230211
}
@@ -314,13 +295,6 @@
314295
]
315296
}
316297
}
317-
},
318-
{
319-
"name": "petId",
320-
"int64": {
321-
"computed_optional_required": "computed_optional",
322-
"description": "ID of pet to return"
323-
}
324298
}
325299
]
326300
}

internal/cmd/testdata/petstore3/tfopenapigen_config.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ resources:
1515
delete:
1616
path: /pet/{petId}
1717
method: DELETE
18+
schema:
19+
attributes:
20+
aliases:
21+
petId: id
1822

1923
order:
2024
create:
@@ -26,6 +30,10 @@ resources:
2630
delete:
2731
path: /store/order/{orderId}
2832
method: DELETE
33+
schema:
34+
attributes:
35+
aliases:
36+
orderId: id
2937

3038
user:
3139
create:
@@ -40,6 +48,10 @@ data_sources:
4048
read:
4149
path: /pet/{petId}
4250
method: GET
51+
schema:
52+
attributes:
53+
aliases:
54+
petId: id
4355

4456
pets:
4557
read:
@@ -50,3 +62,7 @@ data_sources:
5062
read:
5163
path: /store/order/{orderId}
5264
method: GET
65+
schema:
66+
attributes:
67+
aliases:
68+
orderId: id

internal/config/parse.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,29 @@ type Provider struct {
2424
}
2525

2626
type Resource struct {
27-
Create *OpenApiSpecLocation `yaml:"create"`
28-
Read *OpenApiSpecLocation `yaml:"read"`
29-
Update *OpenApiSpecLocation `yaml:"update"`
30-
Delete *OpenApiSpecLocation `yaml:"delete"`
27+
Create *OpenApiSpecLocation `yaml:"create"`
28+
Read *OpenApiSpecLocation `yaml:"read"`
29+
Update *OpenApiSpecLocation `yaml:"update"`
30+
Delete *OpenApiSpecLocation `yaml:"delete"`
31+
SchemaOptions SchemaOptions `yaml:"schema"`
3132
}
32-
3333
type DataSource struct {
34-
Read *OpenApiSpecLocation `yaml:"read"`
34+
Read *OpenApiSpecLocation `yaml:"read"`
35+
SchemaOptions SchemaOptions `yaml:"schema"`
3536
}
3637

3738
type OpenApiSpecLocation struct {
3839
Path string `yaml:"path"`
3940
Method string `yaml:"method"`
4041
}
4142

43+
type SchemaOptions struct {
44+
AttributeOptions AttributeOptions `yaml:"attributes"`
45+
}
46+
type AttributeOptions struct {
47+
Aliases map[string]string `yaml:"aliases"`
48+
}
49+
4250
// ParseConfig takes in a byte array (of YAML), unmarshal into a Config struct, and validates the result
4351
func ParseConfig(bytes []byte) (*Config, error) {
4452
var result Config

internal/config/parse_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,24 @@ resources:
2929
read:
3030
path: /example/path/to/thing/{id}
3131
method: GET`,
32+
},
33+
"valid resource with parameter matches": {
34+
input: `
35+
provider:
36+
name: example
37+
38+
resources:
39+
thing:
40+
create:
41+
path: /example/path/to/things
42+
method: POST
43+
read:
44+
path: /example/path/to/thing/{id}
45+
method: GET
46+
schema:
47+
attributes:
48+
aliases:
49+
otherId: id`,
3250
},
3351
"valid single data source": {
3452
input: `
@@ -40,6 +58,21 @@ data_sources:
4058
read:
4159
path: /example/path/to/thing/{id}
4260
method: GET`,
61+
},
62+
"valid data source with parameter matches": {
63+
input: `
64+
provider:
65+
name: example
66+
67+
data_sources:
68+
thing:
69+
read:
70+
path: /example/path/to/thing/{id}
71+
method: GET
72+
schema:
73+
attributes:
74+
aliases:
75+
otherId: id`,
4376
},
4477
"valid combo of resources and data sources": {
4578
input: `

internal/explorer/config_explorer.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,14 @@ func (e configExplorer) FindProvider() (Provider, error) {
5454

5555
func (e configExplorer) FindResources() (map[string]Resource, error) {
5656
resources := map[string]Resource{}
57-
for name, opMapping := range e.config.Resources {
57+
for name, resourceConfig := range e.config.Resources {
5858
// TODO: should we throw an error if an invalid or non-existent path/methods are given?
5959
resources[name] = Resource{
60-
CreateOp: extractOp(e.spec.Paths, opMapping.Create),
61-
ReadOp: extractOp(e.spec.Paths, opMapping.Read),
62-
UpdateOp: extractOp(e.spec.Paths, opMapping.Update),
63-
DeleteOp: extractOp(e.spec.Paths, opMapping.Delete),
60+
CreateOp: extractOp(e.spec.Paths, resourceConfig.Create),
61+
ReadOp: extractOp(e.spec.Paths, resourceConfig.Read),
62+
UpdateOp: extractOp(e.spec.Paths, resourceConfig.Update),
63+
DeleteOp: extractOp(e.spec.Paths, resourceConfig.Delete),
64+
SchemaOptions: extractSchemaOptions(resourceConfig.SchemaOptions),
6465
}
6566
}
6667

@@ -69,9 +70,10 @@ func (e configExplorer) FindResources() (map[string]Resource, error) {
6970

7071
func (e configExplorer) FindDataSources() (map[string]DataSource, error) {
7172
dataSources := map[string]DataSource{}
72-
for name, opMapping := range e.config.DataSources {
73+
for name, dataSourceConfig := range e.config.DataSources {
7374
dataSources[name] = DataSource{
74-
ReadOp: extractOp(e.spec.Paths, opMapping.Read),
75+
ReadOp: extractOp(e.spec.Paths, dataSourceConfig.Read),
76+
SchemaOptions: extractSchemaOptions(dataSourceConfig.SchemaOptions),
7577
}
7678
}
7779
return dataSources, nil
@@ -130,3 +132,11 @@ func extractSchemaProxy(document high.Document, componentRef string) (*highbase.
130132
// wrap in a schema proxy for mapping with `oas` package
131133
return highbase.CreateSchemaProxy(highSchema), nil
132134
}
135+
136+
func extractSchemaOptions(cfgSchemaOpts config.SchemaOptions) SchemaOptions {
137+
return SchemaOptions{
138+
AttributeOptions: AttributeOptions{
139+
Aliases: cfgSchemaOpts.AttributeOptions.Aliases,
140+
},
141+
}
142+
}

0 commit comments

Comments
 (0)