-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* working with basic generation, still needs decorators * declarations schema to bicep (#7) * update test files, allowed to enum * declare description * reformatting, adding min/max value support * moving creating param line to helper function * trying to handle defaults * fix defaults (strings), still fixing min/max lengths * making test easier to see * fixing default int, prefixing description with sys to avoid collisions * removing notes about defaults * moving comment about description decorator * min/max len string and secure in * min/max len array, default array * moving default array complexity to new helper * default object (almost) need to fix map order * default object working * cleaning up var names * allowed objects working * splitting out functionality to helper for allowed object * refactor pass 1, everything but array/object/allowed optimized * renaming functions, moving them around, fixed arrays * renamed more functions * allowed arrays/objects passing, but need to refactor * remove unneeded test, sloppily solved default objects/arrays * New tests, removing some comments * simplified parseArray & parseObject, just need to handle nest levels * Fixing nest levels after refactoring - Updated renderBicep, parseArray, and parseObject to pass the prefix level back and forth to maintain nesting - Updated test JSON for multi-level nested objects and arrays - Updated test bicep for multi-level nested objects and arrays * Fixed spacing for all but last closing bracket * Fixed deep nesting for objects/arrays * adding error handling, restructured order of functions * Fixes based on suggestions * errors.New -> fmt.Errorf * removing excess error calling * Merging a function, handling some errors --------- Co-authored-by: chrisghill <chris@massdriver.cloud>
- Loading branch information
1 parent
8c378b7
commit 5d04f60
Showing
5 changed files
with
734 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
package bicep | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"reflect" | ||
|
||
"github.com/massdriver-cloud/airlock/pkg/schema" | ||
) | ||
|
||
var indent string = " " | ||
|
||
func SchemaToBicep(in io.Reader) ([]byte, error) { | ||
inBytes, err := io.ReadAll(in) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
root := schema.Schema{} | ||
err = json.Unmarshal(inBytes, &root) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
content := bytes.NewBuffer(nil) | ||
|
||
flattenedProperties := schema.ExpandProperties(&root) | ||
for prop := flattenedProperties.Oldest(); prop != nil; prop = prop.Next() { | ||
err = createBicepParameter(prop.Key, prop.Value, content) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
return content.Bytes(), nil | ||
} | ||
|
||
func createBicepParameter(name string, sch *schema.Schema, buf *bytes.Buffer) error { | ||
bicepType, err := getBicepTypeFromSchema(sch.Type) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
writeDescription(sch, buf) | ||
if allowParamErr := writeAllowedParams(sch, buf); allowParamErr != nil { | ||
return allowParamErr | ||
} | ||
writeMinValue(sch, buf, bicepType) | ||
writeMaxValue(sch, buf, bicepType) | ||
writeMinLength(sch, buf, bicepType) | ||
writeMaxLength(sch, buf, bicepType) | ||
writeSecure(sch, buf, bicepType) | ||
return writeBicepParam(name, sch, buf, bicepType) | ||
} | ||
|
||
func writeBicepParam(name string, sch *schema.Schema, buf *bytes.Buffer, bicepType string) error { | ||
var defVal string | ||
|
||
if sch.Default != nil { | ||
renderedVal, err := renderBicep(sch.Default, "") | ||
if err != nil { | ||
return err | ||
} | ||
|
||
defVal = fmt.Sprintf(" = %s", renderedVal) | ||
} | ||
|
||
buf.WriteString(fmt.Sprintf("param %s %s%s\n", name, bicepType, defVal)) | ||
return nil | ||
} | ||
|
||
func renderBicep(val interface{}, prefix string) (string, error) { | ||
switch reflect.TypeOf(val).Kind() { | ||
case reflect.String: | ||
return fmt.Sprintf("'%s'", val), nil | ||
case reflect.Float64: | ||
return fmt.Sprintf("%v", val), nil | ||
case reflect.Bool: | ||
return fmt.Sprintf("%v", val), nil | ||
case reflect.Slice: | ||
assertedVal, asserArrErr := val.([]interface{}) | ||
if asserArrErr != true { | ||
return "", fmt.Errorf("unable to convert value into array: %v", val) | ||
} | ||
|
||
return parseArray(assertedVal, prefix) | ||
case reflect.Map: | ||
assertedVal, asserObjErr := val.(map[string]interface{}) | ||
if asserObjErr != true { | ||
return "", fmt.Errorf("unable to convert value into object: %v", val) | ||
} | ||
|
||
return parseObject(assertedVal, prefix) | ||
default: | ||
return "", errors.New("unknown type: " + reflect.TypeOf(val).Kind().String()) | ||
} | ||
} | ||
|
||
func getBicepTypeFromSchema(schemaType string) (string, error) { | ||
switch schemaType { | ||
case "string": | ||
return "string", nil | ||
case "integer", "number": | ||
return "int", nil | ||
case "boolean": | ||
return "bool", nil | ||
case "object", "": | ||
return "object", nil | ||
case "array": | ||
return "array", nil | ||
default: | ||
return "", errors.New("unknown type: " + schemaType) | ||
} | ||
} | ||
|
||
func writeDescription(sch *schema.Schema, buf *bytes.Buffer) { | ||
if sch.Description != "" { | ||
// decorators are in sys namespace. to avoid potential collision with other parameters named "description", we use "sys.description" instead of just "description" https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/parameters#decorators | ||
buf.WriteString(fmt.Sprintf("@sys.description('%s')\n", sch.Description)) | ||
} | ||
} | ||
|
||
func writeAllowedParams(sch *schema.Schema, buf *bytes.Buffer) error { | ||
if sch.Enum != nil && len(sch.Enum) > 0 { | ||
renderedVal, err := renderBicep(sch.Enum, "") | ||
if err != nil { | ||
return err | ||
} | ||
|
||
buf.WriteString(fmt.Sprintf("@allowed(%s)\n", renderedVal)) | ||
} | ||
return nil | ||
} | ||
|
||
func writeMinValue(sch *schema.Schema, buf *bytes.Buffer, bicepType string) { | ||
if bicepType == "int" && sch.Minimum != "" { | ||
// set this to %v because sch.Minimum uses json.Number type | ||
buf.WriteString(fmt.Sprintf("@minValue(%v)\n", sch.Minimum)) | ||
} | ||
} | ||
|
||
func writeMaxValue(sch *schema.Schema, buf *bytes.Buffer, bicepType string) { | ||
if bicepType == "int" && sch.Maximum != "" { | ||
buf.WriteString(fmt.Sprintf("@maxValue(%v)\n", sch.Maximum)) | ||
} | ||
} | ||
|
||
func writeMinLength(sch *schema.Schema, buf *bytes.Buffer, bicepType string) { | ||
switch bicepType { | ||
case "array": | ||
if sch.MinItems != nil { | ||
buf.WriteString(fmt.Sprintf("@minLength(%d)\n", *sch.MinItems)) | ||
} | ||
case "string": | ||
if sch.MinLength != nil { | ||
buf.WriteString(fmt.Sprintf("@minLength(%d)\n", *sch.MinLength)) | ||
} | ||
} | ||
} | ||
|
||
func writeMaxLength(sch *schema.Schema, buf *bytes.Buffer, bicepType string) { | ||
switch bicepType { | ||
case "array": | ||
if sch.MaxItems != nil { | ||
buf.WriteString(fmt.Sprintf("@maxLength(%d)\n", *sch.MaxItems)) | ||
} | ||
case "string": | ||
if sch.MaxLength != nil { | ||
buf.WriteString(fmt.Sprintf("@maxLength(%d)\n", *sch.MaxLength)) | ||
} | ||
} | ||
} | ||
|
||
func writeSecure(sch *schema.Schema, buf *bytes.Buffer, bicepType string) { | ||
if bicepType == "string" && sch.Format == "password" { | ||
buf.WriteString("@secure()\n") | ||
} | ||
} | ||
|
||
func parseArray(arr []interface{}, prefix string) (string, error) { | ||
parsedArr := "[\n" | ||
|
||
for _, v := range arr { | ||
renderedVal, err := renderBicep(v, prefix+indent) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
parsedArr += fmt.Sprintf("%s%s", prefix+indent, renderedVal) + "\n" | ||
} | ||
|
||
parsedArr += fmt.Sprintf("%s]", prefix) | ||
return parsedArr, nil | ||
} | ||
|
||
func parseObject(obj map[string]interface{}, prefix string) (string, error) { | ||
parsedObj := "{\n" | ||
|
||
for k, v := range obj { | ||
renderedVal, err := renderBicep(v, prefix+indent) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
parsedObj += fmt.Sprintf("%s%s: %s", prefix+indent, k, renderedVal) + "\n" | ||
} | ||
|
||
parsedObj += fmt.Sprintf("%s}", prefix) | ||
return parsedObj, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
package bicep_test | ||
|
||
import ( | ||
"os" | ||
"path/filepath" | ||
"testing" | ||
|
||
"github.com/massdriver-cloud/airlock/pkg/bicep" | ||
) | ||
|
||
func TestSchemaToBicep(t *testing.T) { | ||
type testData struct { | ||
name string | ||
} | ||
tests := []testData{ | ||
{ | ||
name: "simple", | ||
}, | ||
} | ||
for _, tc := range tests { | ||
t.Run(tc.name, func(t *testing.T) { | ||
want, err := os.ReadFile(filepath.Join("testdata", tc.name+".bicep")) | ||
if err != nil { | ||
t.Fatalf("%d, unexpected error", err) | ||
} | ||
|
||
schemaFile, err := os.Open(filepath.Join("testdata", tc.name+".json")) | ||
if err != nil { | ||
t.Fatalf("%d, unexpected error", err) | ||
} | ||
|
||
got, err := bicep.SchemaToBicep(schemaFile) | ||
if err != nil { | ||
t.Fatalf("%d, unexpected error", err) | ||
} | ||
|
||
if string(got) != string(want) { | ||
t.Fatalf("\ngot: %q\n want: %q", string(got), string(want)) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
param stringtest string | ||
param integertest int | ||
param numbertest int | ||
param booltest bool | ||
param arraytest array | ||
param objecttest object | ||
param nestedtest object | ||
@allowed([ | ||
'foo' | ||
'bar' | ||
]) | ||
param enumtest string | ||
@allowed([ | ||
1 | ||
2 | ||
]) | ||
param enumtestints int | ||
@allowed([ | ||
true | ||
false | ||
]) | ||
param enumtestbools bool | ||
@allowed([ | ||
[ | ||
'foo' | ||
'bar' | ||
] | ||
[ | ||
'baz' | ||
'qux' | ||
] | ||
]) | ||
param enumtestarrays array | ||
@allowed([ | ||
{ | ||
foo: 'bar' | ||
} | ||
{ | ||
baz: 'qux' | ||
} | ||
]) | ||
param enumobjecttest object | ||
@sys.description('This is a description') | ||
param descriptiontest string | ||
@sys.description('This is a new description') | ||
@allowed([ | ||
'foo' | ||
'bar' | ||
'baz' | ||
]) | ||
param descriptionenumtest string | ||
@minValue(5) | ||
param minvaluetest int | ||
@maxValue(10) | ||
param maxvaluetest int | ||
@minValue(5) | ||
@maxValue(10) | ||
param minmaxvaluetest int | ||
@minLength(5) | ||
param minlengthstringtest string | ||
@maxLength(10) | ||
param maxlengthstringtest string | ||
@minLength(5) | ||
@maxLength(10) | ||
param minmaxlengthstringtest string | ||
@minLength(2) | ||
param minlengtharraytest array | ||
@maxLength(5) | ||
param maxlengtharraytest array | ||
@minLength(2) | ||
@maxLength(5) | ||
param minmaxlengtharraytest array | ||
param defaultstringtest string = 'foo' | ||
param defaultintegertest int = 5 | ||
param defaultbooltest bool = true | ||
param defaultarraytest array = [ | ||
'foo' | ||
'bar' | ||
] | ||
param defaultobjecttest object = { | ||
bar: 'baz' | ||
foo: 5 | ||
} | ||
param defaultspaceobjecttest object = { | ||
foo: 'bar baz' | ||
lorem: 'ipsum' | ||
} | ||
param defaultarrayobjecttest array = [ | ||
{ | ||
bar: 'baz' | ||
foo: 5 | ||
} | ||
{ | ||
bar: 'qux' | ||
foo: 10 | ||
} | ||
] | ||
param defaultnestedarraytest array = [ | ||
[ | ||
[ | ||
'foo' | ||
] | ||
[ | ||
'bar' | ||
] | ||
] | ||
[ | ||
[ | ||
'baz' | ||
] | ||
[ | ||
'qux' | ||
] | ||
] | ||
] | ||
param defaultnestedobjecttest object = { | ||
foo: { | ||
bar: { | ||
baz: 'qux' | ||
} | ||
} | ||
quid: { | ||
pro: 'quo' | ||
} | ||
} | ||
@secure() | ||
param securestringtest string |
Oops, something went wrong.