Skip to content

Commit

Permalink
Add schematobicep (#8)
Browse files Browse the repository at this point in the history
* 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
mclacore and chrisghill authored Sep 6, 2024
1 parent 8c378b7 commit 5d04f60
Show file tree
Hide file tree
Showing 5 changed files with 734 additions and 6 deletions.
213 changes: 213 additions & 0 deletions pkg/bicep/schematobicep.go
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
}
42 changes: 42 additions & 0 deletions pkg/bicep/schematobicep_test.go
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))
}
})
}
}
127 changes: 127 additions & 0 deletions pkg/bicep/testdata/simple.bicep
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
Loading

0 comments on commit 5d04f60

Please sign in to comment.