diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml deleted file mode 100644 index 2d840ea..0000000 --- a/.github/workflows/pre-commit.yaml +++ /dev/null @@ -1,19 +0,0 @@ -name: pre-commit - -on: - pull_request: - push: - branches: [main] - -jobs: - pre-commit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v2 - - uses: pre-commit/action@v2.0.3 - env: - # this check prevents devs from commit to main. - # however, we don't want it to fail on commits to main in CI. - # we use the golangci-lint gh action in lint.yaml because it generates useful comments. - SKIP: no-commit-to-branch,golangci-lint diff --git a/.golangci.yaml b/.golangci.yaml index f2b29ab..97e8e49 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -246,4 +246,4 @@ issues: - noctx - wrapcheck - gocognit - - cyclop + - cyclop \ No newline at end of file diff --git a/cmd/bicep.go b/cmd/bicep.go index b57c0ed..7187bb6 100644 --- a/cmd/bicep.go +++ b/cmd/bicep.go @@ -1,7 +1,6 @@ package cmd import ( - "encoding/json" "fmt" "os" @@ -42,17 +41,11 @@ func NewCmdBicep() *cobra.Command { } func runBicepInput(cmd *cobra.Command, args []string) error { - schema, err := bicep.BicepToSchema(args[0]) - if err != nil { - return err - } + result := bicep.BicepToSchema(args[0]) - bytes, err := json.MarshalIndent(schema, "", " ") - if err != nil { - return err - } + fmt.Print(result.PrettyDiags()) + fmt.Print(result.PrettySchema()) - fmt.Println(string(bytes)) return nil } diff --git a/cmd/helm.go b/cmd/helm.go index 97bf9b9..5879cc4 100644 --- a/cmd/helm.go +++ b/cmd/helm.go @@ -1,7 +1,6 @@ package cmd import ( - "encoding/json" "fmt" "github.com/massdriver-cloud/airlock/docs/helpdocs" @@ -31,16 +30,10 @@ func NewCmdHelm() *cobra.Command { } func runHelmInput(cmd *cobra.Command, args []string) error { - schema, err := helm.HelmToSchema(args[0]) - if err != nil { - return err - } + result := helm.HelmToSchema(args[0]) - bytes, err := json.MarshalIndent(schema, "", " ") - if err != nil { - return err - } + fmt.Print(result.PrettyDiags()) + fmt.Print(result.PrettySchema()) - fmt.Println(string(bytes)) return nil } diff --git a/cmd/opentofu.go b/cmd/opentofu.go index 9ddb373..e8041f2 100644 --- a/cmd/opentofu.go +++ b/cmd/opentofu.go @@ -1,7 +1,6 @@ package cmd import ( - "encoding/json" "fmt" "os" @@ -43,17 +42,11 @@ func NewCmdOpenTofu() *cobra.Command { } func runOpenTofuInput(cmd *cobra.Command, args []string) error { - schema, err := opentofu.TofuToSchema(args[0]) - if err != nil { - return err - } + result := opentofu.TofuToSchema(args[0]) - bytes, err := json.MarshalIndent(schema, "", " ") - if err != nil { - return err - } + fmt.Print(result.PrettyDiags()) + fmt.Print(result.PrettySchema()) - fmt.Println(string(bytes)) return nil } diff --git a/go.mod b/go.mod index 4cc8a08..7d5af25 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/hashicorp/hcl/v2 v2.22.0 github.com/massdriver-cloud/terraform-config-inspect v0.0.1 github.com/spf13/cobra v1.8.1 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 github.com/wk8/go-ordered-map/v2 v2.1.8 github.com/xeipuuv/gojsonschema v1.2.0 github.com/zclconf/go-cty v1.15.0 diff --git a/go.sum b/go.sum index ef9e2ff..a6f54c6 100644 --- a/go.sum +++ b/go.sum @@ -105,8 +105,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= diff --git a/pkg/bicep/biceptoschema.go b/pkg/bicep/biceptoschema.go index e220b12..3084260 100644 --- a/pkg/bicep/biceptoschema.go +++ b/pkg/bicep/biceptoschema.go @@ -2,12 +2,12 @@ package bicep import ( "encoding/json" - "errors" "fmt" "reflect" "slices" bp "github.com/Checkmarx/kics/v2/pkg/parser/bicep" + "github.com/massdriver-cloud/airlock/pkg/result" "github.com/massdriver-cloud/airlock/pkg/schema" orderedmap "github.com/wk8/go-ordered-map/v2" @@ -29,81 +29,119 @@ type bicepParamMetadata struct { Metadata map[string]interface{} `json:"metadata"` } -func BicepToSchema(templatePath string) (*schema.Schema, error) { +func BicepToSchema(templatePath string) result.SchemaResult { // using the github.com/Checkmarx/kics parser since he already did the heavy lifting to parse a bicep template parser := bp.Parser{} - params := new(schema.Schema) - params.Type = "object" - params.Properties = orderedmap.New[string, *schema.Schema]() - params.Required = []string{} + sch := new(schema.Schema) + sch.Type = "object" + sch.Properties = orderedmap.New[string, *schema.Schema]() + sch.Required = []string{} - doc, _, err := parser.Parse(templatePath, nil) - if err != nil { - return nil, err + doc, _, parseErr := parser.Parse(templatePath, nil) + if parseErr != nil { + return result.SchemaResult{ + Schema: nil, + Diags: []result.Diagnostic{ + { + Path: templatePath, + Code: "file_read_error", + Message: fmt.Sprintf("failed to read bicep file: %s", parseErr), + Level: result.Error, + }, + }, + } + } + + output := result.SchemaResult{ + Schema: sch, + Diags: []result.Diagnostic{}, } for name, value := range doc[0]["parameters"].(map[string]interface{}) { - param := bicepParam{} + param := new(bicepParam) // marshal to json and unmarshal into custom struct to make bicep param easier to access bytes, marshalErr := json.Marshal(value) if marshalErr != nil { - return nil, marshalErr + output.Diags = append(output.Diags, result.Diagnostic{ + Path: name, + Code: "invalid_value", + Message: fmt.Sprintf("failed to marshal bicep param %s: %s", name, marshalErr), + Level: result.Error, + }) + continue } unmarshalErr := json.Unmarshal(bytes, ¶m) if unmarshalErr != nil { - return nil, unmarshalErr + output.Diags = append(output.Diags, result.Diagnostic{ + Path: name, + Code: "invalid_value", + Message: fmt.Sprintf("failed to unmarshal bicep param %s: %s", name, unmarshalErr), + Level: result.Error, + }) + continue } property := new(schema.Schema) property.Title = name property.Description = param.Metadata.Description - parseErr := parseBicepParam(property, param) - if parseErr != nil { - return nil, parseErr - } + output.Diags = parseBicepParam(property, param, output.Diags) - params.Properties.Set(name, property) - params.Required = append(params.Required, name) + sch.Properties.Set(name, property) + sch.Required = append(sch.Required, name) } // sorting this here just to help with testing. The order doesn't matter, but to our test suite it does. - slices.Sort(params.Required) + slices.Sort(sch.Required) - return params, nil + return output } -func parseBicepParam(sch *schema.Schema, bicepParam bicepParam) error { +func parseBicepParam(sch *schema.Schema, bicepParam *bicepParam, diags []result.Diagnostic) []result.Diagnostic { switch bicepParam.TypeString { case "int": - return parseIntParam(sch, bicepParam) + return parseIntParam(sch, bicepParam, diags) case "bool": - return parseBoolParam(sch, bicepParam) + parseBoolParam(sch, bicepParam) case "string": - return parseStringParam(sch, bicepParam, false) + return parseStringParam(sch, bicepParam, false, diags) case "secureString": - return parseStringParam(sch, bicepParam, true) + return parseStringParam(sch, bicepParam, true, diags) case "array": - return parseArrayParam(sch, bicepParam) + return parseArrayParam(sch, bicepParam, diags) case "object", "secureObject": - return parseObjectParam(sch, bicepParam) + return parseObjectParam(sch, bicepParam, diags) default: - return errors.New("unknown type: " + bicepParam.TypeString) + sch.Comment = fmt.Sprintf("Airlock Warning: unknown type from Bicep parameter (%s)", bicepParam.TypeString) + return append(diags, result.Diagnostic{ + Path: sch.Title, + Code: "unknown_type", + Message: fmt.Sprintf("type of field %s is unsupported (%s)", sch.Title, bicepParam.TypeString), + Level: result.Warning, + }) } + return diags } -func parseIntParam(sch *schema.Schema, bicepParam bicepParam) error { +func parseIntParam(sch *schema.Schema, bicepParam *bicepParam, diags []result.Diagnostic) []result.Diagnostic { sch.Type = "integer" sch.Default = bicepParam.DefaultValue allowedVals := bicepParam.AllowedValues if len(allowedVals) == 1 { assertedEnum, ok := allowedVals[0].([]interface{}) - if !ok { - return fmt.Errorf("unable to cast %v to []interface{}", allowedVals) + if ok { + sch.Enum = assertedEnum + } else { + sch.Comment = "Airlock Warning: unable to convert 'allowedValues' to enum" + diags = append(diags, result.Diagnostic{ + Path: sch.Title, + Code: "invalid_value", + Message: fmt.Sprintf("unable to convert 'allowedValues' to enum in bicep param %s", sch.Title), + Level: result.Warning, + }) } - sch.Enum = assertedEnum } if bicepParam.MinValue != nil { @@ -113,16 +151,15 @@ func parseIntParam(sch *schema.Schema, bicepParam bicepParam) error { sch.Maximum = json.Number(fmt.Sprintf("%d", *bicepParam.MaxValue)) } - return nil + return diags } -func parseBoolParam(sch *schema.Schema, bicepParam bicepParam) error { +func parseBoolParam(sch *schema.Schema, bicepParam *bicepParam) { sch.Type = "boolean" sch.Default = bicepParam.DefaultValue - return nil } -func parseStringParam(sch *schema.Schema, bicepParam bicepParam, secure bool) error { +func parseStringParam(sch *schema.Schema, bicepParam *bicepParam, secure bool, diags []result.Diagnostic) []result.Diagnostic { sch.Type = "string" sch.Default = bicepParam.DefaultValue @@ -133,46 +170,47 @@ func parseStringParam(sch *schema.Schema, bicepParam bicepParam, secure bool) er allowedVals := bicepParam.AllowedValues if len(allowedVals) == 1 { assertedEnum, ok := allowedVals[0].([]interface{}) - if !ok { - return fmt.Errorf("unable to cast %v to []interface{}", allowedVals) + if ok { + sch.Enum = assertedEnum + } else { + sch.Comment = "Airlock Warning: unable to convert 'allowedValues' to enum" + diags = append(diags, result.Diagnostic{ + Path: sch.Title, + Code: "invalid_value", + Message: fmt.Sprintf("unable to convert 'allowedValues' to enum in bicep param %s", sch.Title), + Level: result.Warning, + }) } - sch.Enum = assertedEnum } sch.MinLength = bicepParam.MinLength sch.MaxLength = bicepParam.MaxLength - return nil + return diags } -func parseArrayParam(sch *schema.Schema, bicepParam bicepParam) error { +func parseArrayParam(sch *schema.Schema, bicepParam *bicepParam, diags []result.Diagnostic) []result.Diagnostic { sch.Type = "array" sch.MinItems = bicepParam.MinLength sch.MaxItems = bicepParam.MaxLength if bicepParam.DefaultValue != nil && len(bicepParam.DefaultValue.([]interface{})) != 0 { - err := parseArrayType(sch, bicepParam.DefaultValue.([]interface{})) - if err != nil { - return err - } + diags = parseArrayType(sch, bicepParam.DefaultValue.([]interface{}), diags) } - return nil + return diags } -func parseObjectParam(sch *schema.Schema, bicepParam bicepParam) error { +func parseObjectParam(sch *schema.Schema, bicepParam *bicepParam, diags []result.Diagnostic) []result.Diagnostic { sch.Type = "object" if bicepParam.DefaultValue != nil && len(bicepParam.DefaultValue.(map[string]interface{})) > 1 { - err := parseObjectType(sch, bicepParam.DefaultValue.(map[string]interface{})) - if err != nil { - return err - } + diags = parseObjectType(sch, bicepParam.DefaultValue.(map[string]interface{}), diags) } - return nil + return diags } -func parseObjectType(sch *schema.Schema, objValue map[string]interface{}) error { +func parseObjectType(sch *schema.Schema, objValue map[string]interface{}, diags []result.Diagnostic) []result.Diagnostic { sch.Properties = orderedmap.New[string, *schema.Schema]() sch.Required = []string{} @@ -196,18 +234,18 @@ func parseObjectType(sch *schema.Schema, objValue map[string]interface{}) error property.Default = value case reflect.Slice: property.Type = "array" - err := parseArrayType(property, value.([]interface{})) - if err != nil { - return err - } + diags = parseArrayType(property, value.([]interface{}), diags) case reflect.Map: property.Type = "object" - err := parseObjectType(property, value.(map[string]interface{})) - if err != nil { - return err - } + diags = parseObjectType(property, value.(map[string]interface{}), diags) default: - return errors.New("unknown type: " + reflect.TypeOf(value).String()) + sch.Comment = fmt.Sprintf("Airlock Warning: unknown type for field %s (%s)", name, reflect.TypeOf(value).Kind()) + diags = append(diags, result.Diagnostic{ + Path: sch.Title, + Code: "unknown_type", + Message: fmt.Sprintf("type of field %s is unsupported (%s)", sch.Title, reflect.TypeOf(value).Kind()), + Level: result.Warning, + }) } sch.Properties.Set(name, property) @@ -215,10 +253,10 @@ func parseObjectType(sch *schema.Schema, objValue map[string]interface{}) error slices.Sort(sch.Required) } - return nil + return diags } -func parseArrayType(sch *schema.Schema, value []interface{}) error { +func parseArrayType(sch *schema.Schema, value []interface{}, diags []result.Diagnostic) []result.Diagnostic { if len(value) > 0 { items := new(schema.Schema) @@ -235,21 +273,21 @@ func parseArrayType(sch *schema.Schema, value []interface{}) error { sch.Default = value case reflect.Slice: items.Type = "array" - err := parseArrayType(items, elem.([]interface{})) - if err != nil { - return err - } + diags = parseArrayType(items, elem.([]interface{}), diags) case reflect.Map: items.Type = "object" - err := parseObjectType(items, elem.(map[string]interface{})) - if err != nil { - return err - } + diags = parseObjectType(items, elem.(map[string]interface{}), diags) default: - return errors.New("unknown type: " + reflect.TypeOf(elem).String()) + sch.Comment = fmt.Sprintf("Airlock Warning: unknown type (%s)", reflect.TypeOf(value).Kind()) + diags = append(diags, result.Diagnostic{ + Path: sch.Title, + Code: "unknown_type", + Message: fmt.Sprintf("type of field %s is unsupported (%s)", sch.Title, reflect.TypeOf(value).Kind()), + Level: result.Warning, + }) } sch.Items = items } - return nil + return diags } diff --git a/pkg/bicep/biceptoschema_test.go b/pkg/bicep/biceptoschema_test.go index 0d9db18..e027256 100644 --- a/pkg/bicep/biceptoschema_test.go +++ b/pkg/bicep/biceptoschema_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/massdriver-cloud/airlock/pkg/bicep" + "github.com/massdriver-cloud/airlock/pkg/result" "github.com/stretchr/testify/assert" ) @@ -12,12 +13,14 @@ func TestBicepToSchema(t *testing.T) { type testData struct { name string bicepPath string + diags []result.Diagnostic want string } tests := []testData{ { name: "simple", bicepPath: "testdata/template.bicep", + diags: []result.Diagnostic{}, want: ` { "required": [ @@ -168,16 +171,15 @@ func TestBicepToSchema(t *testing.T) { } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - got, err := bicep.BicepToSchema(tc.bicepPath) - if err != nil { - t.Fatalf("%d, unexpected error", err) - } + got := bicep.BicepToSchema(tc.bicepPath) - bytes, err := json.Marshal(got) + bytes, err := json.Marshal(got.Schema) if err != nil { t.Fatalf("%d, unexpected error", err) } + assert.ElementsMatch(t, tc.diags, got.Diags) + assert.JSONEq(t, tc.want, string(bytes)) }) } diff --git a/pkg/bicep/schematobicep.go b/pkg/bicep/schematobicep.go index 05fb231..dd1a4a6 100644 --- a/pkg/bicep/schematobicep.go +++ b/pkg/bicep/schematobicep.go @@ -69,7 +69,7 @@ func writeBicepParam(name string, sch *schema.Schema, buf *bytes.Buffer, bicepTy defVal = fmt.Sprintf(" = %s", renderedVal) } - buf.WriteString(fmt.Sprintf("param %s %s%s\n", name, bicepType, defVal)) + fmt.Fprintf(buf, "param %s %s%s\n", name, bicepType, defVal) return nil } @@ -120,7 +120,7 @@ func getBicepTypeFromSchema(schemaType string) (string, error) { 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)) + fmt.Fprintf(buf, "@sys.description('%s')\n", sch.Description) } } @@ -131,7 +131,7 @@ func writeAllowedParams(sch *schema.Schema, buf *bytes.Buffer) error { return err } - buf.WriteString(fmt.Sprintf("@allowed(%s)\n", renderedVal)) + fmt.Fprintf(buf, "@allowed(%s)\n", renderedVal) } return nil } @@ -139,13 +139,13 @@ func writeAllowedParams(sch *schema.Schema, buf *bytes.Buffer) error { 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)) + fmt.Fprintf(buf, "@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)) + fmt.Fprintf(buf, "@maxValue(%v)\n", sch.Maximum) } } @@ -153,11 +153,11 @@ 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)) + fmt.Fprintf(buf, "@minLength(%d)\n", *sch.MinItems) } case "string": if sch.MinLength != nil { - buf.WriteString(fmt.Sprintf("@minLength(%d)\n", *sch.MinLength)) + fmt.Fprintf(buf, "@minLength(%d)\n", *sch.MinLength) } } } @@ -166,11 +166,11 @@ 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)) + fmt.Fprintf(buf, "@maxLength(%d)\n", *sch.MaxItems) } case "string": if sch.MaxLength != nil { - buf.WriteString(fmt.Sprintf("@maxLength(%d)\n", *sch.MaxLength)) + fmt.Fprintf(buf, "@maxLength(%d)\n", *sch.MaxLength) } } } diff --git a/pkg/helm/helmtoschema.go b/pkg/helm/helmtoschema.go index e69105d..12cd1d4 100644 --- a/pkg/helm/helmtoschema.go +++ b/pkg/helm/helmtoschema.go @@ -6,39 +6,56 @@ import ( "strconv" "strings" + "github.com/massdriver-cloud/airlock/pkg/result" "github.com/massdriver-cloud/airlock/pkg/schema" orderedmap "github.com/wk8/go-ordered-map/v2" yaml "gopkg.in/yaml.v3" ) -type nullError struct{} - -func (e *nullError) Error() string { - return "type is indeterminate (null)" -} - -func HelmToSchema(valuesPath string) (*schema.Schema, error) { +func HelmToSchema(valuesPath string) result.SchemaResult { valuesBytes, readErr := os.ReadFile(valuesPath) if readErr != nil { - return nil, readErr + return result.SchemaResult{ + Schema: nil, + Diags: []result.Diagnostic{ + { + Path: valuesPath, + Code: "file_read_error", + Message: fmt.Sprintf("failed to read values file: %s", readErr), + Level: result.Error, + }, + }, + } } valuesDocument := yaml.Node{} unmarshalErr := yaml.Unmarshal(valuesBytes, &valuesDocument) if unmarshalErr != nil { - return nil, unmarshalErr + return result.SchemaResult{ + Schema: nil, + Diags: []result.Diagnostic{ + { + Path: valuesPath, + Code: "yaml_unmarshal_error", + Message: fmt.Sprintf("failed to unmarshal values file: %s", unmarshalErr), + Level: result.Error, + }, + }, + } } - // the top level node is a document node. We need to go one layer - // deeper to get the actual yaml content sch := new(schema.Schema) - err := parseMapNode(sch, valuesDocument.Content[0]) - if err != nil { - return nil, err + result := result.SchemaResult{ + Schema: sch, + Diags: []result.Diagnostic{}, } - return sch, nil + // the top level node is a document node. We need to go one layer + // deeper to get the actual yaml content + result.Diags = parseMapNode(sch, valuesDocument.Content[0], result.Diags) + + return result } func parseNameNode(schema *schema.Schema, node *yaml.Node) { @@ -50,83 +67,99 @@ func parseNameNode(schema *schema.Schema, node *yaml.Node) { } } -func parseValueNode(schema *schema.Schema, node *yaml.Node) error { +func parseValueNode(schema *schema.Schema, node *yaml.Node, diags []result.Diagnostic) []result.Diagnostic { switch node.Tag { case "!!str": - return parseStringNode(schema, node) + parseStringNode(schema, node) case "!!int": - return parseIntegerNode(schema, node) + return parseIntegerNode(schema, node, diags) case "!!float": - return parseFloatNode(schema, node) + return parseFloatNode(schema, node, diags) case "!!bool": - return parseBooleanNode(schema, node) + return parseBooleanNode(schema, node, diags) case "!!map": - return parseMapNode(schema, node) + return parseMapNode(schema, node, diags) case "!!seq": - return parseArrayNode(schema, node) + return parseArrayNode(schema, node, diags) case "!!null": - return &nullError{} + schema.Comment = "Airlock Warning: unknown type from null value" + return append(diags, result.Diagnostic{ + Path: schema.Title, + Code: "unknown_type", + Message: fmt.Sprintf("type of field %s is indeterminate (null)", schema.Title), + Level: result.Warning, + }) default: - return fmt.Errorf("unrecognized tag %s", node.Tag) + schema.Comment = fmt.Sprintf("Airlock Warning: unknown type %s", node.Tag) + return append(diags, result.Diagnostic{ + Path: schema.Title, + Code: "unknown_type", + Message: fmt.Sprintf("type of field %s is unsupported (%s)", schema.Title, node.Tag), + Level: result.Warning, + }) } + return diags } -func nodeToProperty(name, value *yaml.Node) (*schema.Schema, error) { - sch := new(schema.Schema) - +func nodeToProperty(sch *schema.Schema, name, value *yaml.Node, diags []result.Diagnostic) []result.Diagnostic { parseNameNode(sch, name) - err := parseValueNode(sch, value) - if err != nil { - //nolint:errorlint - if _, ok := err.(*nullError); ok { - fmt.Printf("warning: skipping field %s\n reason: %v\n", sch.Title, err) - //nolint:nilnil - return nil, nil - } - return nil, err - } + diags = parseValueNode(sch, value, diags) - return sch, nil + return diags } -func parseStringNode(sch *schema.Schema, node *yaml.Node) error { +func parseStringNode(sch *schema.Schema, node *yaml.Node) { sch.Type = "string" sch.Default = node.Value - return nil } -func parseIntegerNode(sch *schema.Schema, node *yaml.Node) error { +func parseIntegerNode(sch *schema.Schema, node *yaml.Node, diags []result.Diagnostic) []result.Diagnostic { sch.Type = "integer" def, err := strconv.Atoi(node.Value) if err != nil { - return err + return append(diags, result.Diagnostic{ + Path: node.Value, + Code: "invalid_value", + Message: fmt.Sprintf("failed to parse integer: %s", err), + Level: result.Error, + }) } sch.Default = def - return nil + return diags } -func parseFloatNode(sch *schema.Schema, node *yaml.Node) error { +func parseFloatNode(sch *schema.Schema, node *yaml.Node, diags []result.Diagnostic) []result.Diagnostic { sch.Type = "number" def, err := strconv.ParseFloat(node.Value, 64) if err != nil { - return err + return append(diags, result.Diagnostic{ + Path: node.Value, + Code: "invalid_value", + Message: fmt.Sprintf("failed to parse float: %s", err), + Level: result.Error, + }) } sch.Default = def - return nil + return diags } -func parseBooleanNode(sch *schema.Schema, node *yaml.Node) error { +func parseBooleanNode(sch *schema.Schema, node *yaml.Node, diags []result.Diagnostic) []result.Diagnostic { sch.Type = "boolean" def, err := strconv.ParseBool(node.Value) if err != nil { - return err + return append(diags, result.Diagnostic{ + Path: node.Value, + Code: "invalid_value", + Message: fmt.Sprintf("failed to parse boolean: %s", err), + Level: result.Error, + }) } sch.Default = def - return nil + return diags } -func parseMapNode(sch *schema.Schema, node *yaml.Node) error { +func parseMapNode(sch *schema.Schema, node *yaml.Node, diags []result.Diagnostic) []result.Diagnostic { sch.Type = "object" sch.Properties = orderedmap.New[string, *schema.Schema]() @@ -135,36 +168,44 @@ func parseMapNode(sch *schema.Schema, node *yaml.Node) error { for index := 0; index < len(nodes); index += 2 { nameNode := nodes[index] valueNode := nodes[index+1] - property, err := nodeToProperty(nameNode, valueNode) - if err != nil { - return err - } - if property != nil { - sch.Properties.Set(nameNode.Value, property) - sch.Required = append(sch.Required, nameNode.Value) - } + + property := new(schema.Schema) + diags = nodeToProperty(property, nameNode, valueNode, diags) + + sch.Properties.Set(nameNode.Value, property) + sch.Required = append(sch.Required, nameNode.Value) } - return nil + return diags } -func parseArrayNode(sch *schema.Schema, node *yaml.Node) error { +func parseArrayNode(sch *schema.Schema, node *yaml.Node, diags []result.Diagnostic) []result.Diagnostic { sch.Type = "array" - if len(node.Content) == 0 { - return &nullError{} - } sch.Items = new(schema.Schema) - err := parseValueNode(sch.Items, node.Content[0]) - if err != nil { - return err + + if len(node.Content) == 0 { + sch.Items.Comment = "Airlock Warning: unknown type from empty array" + return append(diags, result.Diagnostic{ + Path: sch.Title, + Code: "unknown_type", + Message: fmt.Sprintf("array %s is empty so it's type is unknown", sch.Title), + Level: result.Warning, + }) } + diags = parseValueNode(sch.Items, node.Content[0], diags) + // Set the default back to nil since we don't want to default all items to the first type in the list - err = node.Decode(&sch.Default) - if err != nil { - return err + decodeErr := node.Decode(&sch.Default) + if decodeErr != nil { + return append(diags, result.Diagnostic{ + Path: sch.Title, + Code: "invalid_type", + Message: fmt.Sprintf("failed to decode array default: %s", decodeErr), + Level: result.Error, + }) } - return nil + return diags } diff --git a/pkg/helm/helmtoschema_test.go b/pkg/helm/helmtoschema_test.go index a5cab1e..23f45c9 100644 --- a/pkg/helm/helmtoschema_test.go +++ b/pkg/helm/helmtoschema_test.go @@ -5,19 +5,36 @@ import ( "testing" "github.com/massdriver-cloud/airlock/pkg/helm" + "github.com/massdriver-cloud/airlock/pkg/result" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestRun(t *testing.T) { type testData struct { name string - modulePath string + valuesPath string + diags []result.Diagnostic want string } tests := []testData{ { name: "simple", - modulePath: "testdata/values.yaml", + valuesPath: "testdata/values.yaml", + diags: []result.Diagnostic{ + { + Path: "emptyArray", + Code: "unknown_type", + Message: "array emptyArray is empty so it's type is unknown", + Level: result.Warning, + }, + { + Path: "nullValue", + Code: "unknown_type", + Message: "type of field nullValue is indeterminate (null)", + Level: result.Warning, + }, + }, want: ` { "required": [ @@ -25,7 +42,9 @@ func TestRun(t *testing.T) { "age", "height", "object", - "array" + "array", + "emptyArray", + "nullValue" ], "type": "object", "properties": { @@ -81,6 +100,18 @@ func TestRun(t *testing.T) { "foo", "bar" ] + }, + "emptyArray": { + "title": "emptyArray", + "type": "array", + "description": "An empty array should not cause an error", + "items": { + "$comment": "Airlock Warning: unknown type from empty array" + } + }, + "nullValue": { + "title": "nullValue", + "$comment": "Airlock Warning: unknown type from null value" } } } @@ -89,17 +120,16 @@ func TestRun(t *testing.T) { } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - got, err := helm.HelmToSchema(tc.modulePath) - if err != nil { - t.Fatalf("%d, unexpected error", err) - } + got := helm.HelmToSchema(tc.valuesPath) - bytes, err := json.Marshal(got) + bytes, err := json.Marshal(got.Schema) if err != nil { t.Fatalf("%d, unexpected error", err) } require.JSONEq(t, tc.want, string(bytes)) + + assert.ElementsMatch(t, tc.diags, got.Diags) }) } } diff --git a/pkg/opentofu/testdata/opentofu/any/variables.tf b/pkg/opentofu/testdata/opentofu/any/variables.tf deleted file mode 100644 index 53521e6..0000000 --- a/pkg/opentofu/testdata/opentofu/any/variables.tf +++ /dev/null @@ -1,3 +0,0 @@ -variable "foo" { - type = any -} \ No newline at end of file diff --git a/pkg/opentofu/testdata/opentofu/empty/variables.tf b/pkg/opentofu/testdata/opentofu/empty/variables.tf deleted file mode 100644 index 27d4129..0000000 --- a/pkg/opentofu/testdata/opentofu/empty/variables.tf +++ /dev/null @@ -1 +0,0 @@ -variable "foo" {} \ No newline at end of file diff --git a/pkg/opentofu/testdata/opentofu/nestedany/variables.tf b/pkg/opentofu/testdata/opentofu/nestedany/variables.tf deleted file mode 100644 index 1f099e0..0000000 --- a/pkg/opentofu/testdata/opentofu/nestedany/variables.tf +++ /dev/null @@ -1,5 +0,0 @@ -variable "foo" { - type = object({ - bar = any - }) -} \ No newline at end of file diff --git a/pkg/opentofu/testdata/opentofu/simple/schema.json b/pkg/opentofu/testdata/opentofu/simple/schema.json index d64aae2..b047603 100644 --- a/pkg/opentofu/testdata/opentofu/simple/schema.json +++ b/pkg/opentofu/testdata/opentofu/simple/schema.json @@ -1,5 +1,8 @@ { "required": [ + "any", + "empty", + "nestedany", "nodescription", "testbool", "testemptybool", @@ -197,6 +200,27 @@ "nodescription": { "title": "nodescription", "type": "string" + }, + "any": { + "title": "any", + "$comment": "Airlock warning: unconstrained type from OpenTofu/Terraform 'any'" + }, + "nestedany": { + "title": "nestedany", + "type": "object", + "properties": { + "foo": { + "title": "foo", + "$comment": "Airlock warning: unconstrained type from OpenTofu/Terraform 'any'" + } + }, + "required": [ + "foo" + ] + }, + "empty": { + "title": "empty", + "$comment": "Airlock warning: unconstrained type from OpenTofu/Terraform 'any'" } } } diff --git a/pkg/opentofu/testdata/opentofu/simple/variables.tf b/pkg/opentofu/testdata/opentofu/simple/variables.tf index 182f718..4ae54d0 100644 --- a/pkg/opentofu/testdata/opentofu/simple/variables.tf +++ b/pkg/opentofu/testdata/opentofu/simple/variables.tf @@ -89,3 +89,15 @@ variable "testmap" { variable "nodescription" { type = string } + +variable "any" { + type = any +} + +variable "nestedany" { + type = object({ + foo = any + }) +} + +variable "empty" {} diff --git a/pkg/opentofu/tofutoschema.go b/pkg/opentofu/tofutoschema.go index 2ec509c..b80d8ec 100644 --- a/pkg/opentofu/tofutoschema.go +++ b/pkg/opentofu/tofutoschema.go @@ -9,6 +9,7 @@ import ( hcl "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/ext/typeexpr" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/massdriver-cloud/airlock/pkg/result" "github.com/massdriver-cloud/airlock/pkg/schema" "github.com/massdriver-cloud/terraform-config-inspect/tfconfig" orderedmap "github.com/wk8/go-ordered-map/v2" @@ -16,34 +17,58 @@ import ( ctyjson "github.com/zclconf/go-cty/cty/json" ) -func TofuToSchema(modulePath string) (*schema.Schema, error) { +func TofuToSchema(modulePath string) result.SchemaResult { module, err := tfconfig.LoadModule(modulePath) if err != nil { - return nil, err + return result.SchemaResult{ + Schema: nil, + Diags: []result.Diagnostic{ + { + Path: modulePath, + Code: "module_load_error", + Message: fmt.Sprintf("failed to load module: %s", err), + Level: result.Error, + }, + }, + } } sch := new(schema.Schema) sch.Properties = orderedmap.New[string, *schema.Schema]() + result := result.SchemaResult{ + Schema: sch, + Diags: []result.Diagnostic{}, + } + for _, variable := range module.Variables { - variableSchema, err := variableToSchema(variable) - if err != nil { - return nil, fmt.Errorf("failed to convert variable %q to schema: %w", variable.Name, err) + variableSchema, diags := variableToSchema(variable, result.Diags) + result.Diags = diags + + if variableSchema == nil { + continue } + sch.Properties.Set(variable.Name, variableSchema) sch.Required = append(sch.Required, variable.Name) } slices.Sort(sch.Required) - return sch, nil + return result } -func variableToSchema(variable *tfconfig.Variable) (*schema.Schema, error) { +func variableToSchema(variable *tfconfig.Variable, diags []result.Diagnostic) (*schema.Schema, []result.Diagnostic) { schema := new(schema.Schema) variableType, defaults, typeErr := variableTypeStringToCtyType(variable.Type) if typeErr != nil { - return nil, fmt.Errorf("failed to parse type %q: %w", variable.Type, typeErr) + diags = append(diags, result.Diagnostic{ + Path: variable.Name, + Code: "variable_type_error", + Message: fmt.Sprintf("failed to parse type %q: %s", variable.Type, typeErr), + Level: result.Error, + }) + return nil, diags } // To simplify the logic of recursively walking the Defaults structure in objects types, // we make the extracted Defaults a Child of a dummy "top level" node @@ -54,9 +79,7 @@ func variableToSchema(variable *tfconfig.Variable) (*schema.Schema, error) { variable.Name: defaults, } } - if hydrateErr := hydrateSchemaFromNameTypeAndDefaults(schema, variable.Name, variableType, topLevelDefault); hydrateErr != nil { - return nil, fmt.Errorf("failed to hydrate schema for variable %q: %w", variable.Name, hydrateErr) - } + diags = hydrateSchemaFromNameTypeAndDefaults(schema, variable.Name, variableType, topLevelDefault, diags) schema.Description = variable.Description @@ -68,15 +91,12 @@ func variableToSchema(variable *tfconfig.Variable) (*schema.Schema, error) { schema.Default = false } - return schema, nil + return schema, diags } func variableTypeStringToCtyType(variableType string) (cty.Type, *typeexpr.Defaults, error) { if variableType == "" { - return cty.NilType, nil, errors.New("type cannot be empty") - } - if variableType == "any" { - return cty.NilType, nil, errors.New("type 'any' cannot be converted to a JSON schema type") + variableType = "any" } expr, diags := hclsyntax.ParseExpression([]byte(variableType), "", hcl.Pos{Line: 1, Column: 1}) if len(diags) != 0 { @@ -89,7 +109,7 @@ func variableTypeStringToCtyType(variableType string) (cty.Type, *typeexpr.Defau return ty, defaults, nil } -func hydrateSchemaFromNameTypeAndDefaults(sch *schema.Schema, name string, ty cty.Type, defaults *typeexpr.Defaults) error { +func hydrateSchemaFromNameTypeAndDefaults(sch *schema.Schema, name string, ty cty.Type, defaults *typeexpr.Defaults, diags []result.Diagnostic) []result.Diagnostic { sch.Title = name if defaults != nil { @@ -102,19 +122,25 @@ func hydrateSchemaFromNameTypeAndDefaults(sch *schema.Schema, name string, ty ct if ty.IsPrimitiveType() { hydratePrimitiveSchema(sch, ty) } else if ty.IsMapType() { - return hydrateMapSchema(sch, name, ty, defaults) + return hydrateMapSchema(sch, name, ty, defaults, diags) } else if ty.IsObjectType() { - return hydrateObjectSchema(sch, name, ty, defaults) + return hydrateObjectSchema(sch, name, ty, defaults, diags) } else if ty.IsListType() { - return hydrateArraySchema(sch, name, ty, defaults) + return hydrateArraySchema(sch, name, ty, defaults, diags) } else if ty.IsSetType() { - return hydrateSetSchema(sch, name, ty, defaults) + return hydrateSetSchema(sch, name, ty, defaults, diags) } else if ty.HasDynamicTypes() { - return fmt.Errorf("dynamic types are not supported (are you using type 'any'?)") + return hydrateAnySchema(sch, diags) } else { - return fmt.Errorf("unsupported type %q", ty.FriendlyName()) - } - return nil + sch.Comment = fmt.Sprintf("unsupported OpenTofu/Terraform type '%s'", ty.FriendlyName()) + return append(diags, result.Diagnostic{ + Path: name, + Code: "unsupported_type", + Message: fmt.Sprintf("unsupported OpenTofu/Terraform type '%s' in field '%s'", ty.FriendlyName(), name), + Level: result.Warning, + }) + } + return diags } func hydratePrimitiveSchema(sch *schema.Schema, ty cty.Type) { @@ -128,41 +154,49 @@ func hydratePrimitiveSchema(sch *schema.Schema, ty cty.Type) { } } -func hydrateObjectSchema(sch *schema.Schema, name string, ty cty.Type, defaults *typeexpr.Defaults) error { +func hydrateObjectSchema(sch *schema.Schema, name string, ty cty.Type, defaults *typeexpr.Defaults, diags []result.Diagnostic) []result.Diagnostic { sch.Type = "object" sch.Properties = orderedmap.New[string, *schema.Schema]() for attName, attType := range ty.AttributeTypes() { attributeSchema := new(schema.Schema) - if err := hydrateSchemaFromNameTypeAndDefaults(attributeSchema, attName, attType, getDefaultChildren(name, defaults)); err != nil { - return err - } + diags = hydrateSchemaFromNameTypeAndDefaults(attributeSchema, attName, attType, getDefaultChildren(name, defaults), diags) sch.Properties.Set(attName, attributeSchema) if !ty.AttributeOptional(attName) { sch.Required = append(sch.Required, attName) } } slices.Sort(sch.Required) - return nil + return diags } -func hydrateMapSchema(sch *schema.Schema, name string, ty cty.Type, defaults *typeexpr.Defaults) error { +func hydrateMapSchema(sch *schema.Schema, name string, ty cty.Type, defaults *typeexpr.Defaults, diags []result.Diagnostic) []result.Diagnostic { sch.Type = "object" sch.PropertyNames = &schema.Schema{ Pattern: "^.*$", } sch.AdditionalProperties = new(schema.Schema) - return hydrateSchemaFromNameTypeAndDefaults(sch.AdditionalProperties.(*schema.Schema), "", ty.ElementType(), getDefaultChildren(name, defaults)) + return hydrateSchemaFromNameTypeAndDefaults(sch.AdditionalProperties.(*schema.Schema), "", ty.ElementType(), getDefaultChildren(name, defaults), diags) } -func hydrateArraySchema(sch *schema.Schema, name string, ty cty.Type, defaults *typeexpr.Defaults) error { +func hydrateArraySchema(sch *schema.Schema, name string, ty cty.Type, defaults *typeexpr.Defaults, diags []result.Diagnostic) []result.Diagnostic { sch.Type = "array" sch.Items = new(schema.Schema) - return hydrateSchemaFromNameTypeAndDefaults(sch.Items, "", ty.ElementType(), getDefaultChildren(name, defaults)) + return hydrateSchemaFromNameTypeAndDefaults(sch.Items, "", ty.ElementType(), getDefaultChildren(name, defaults), diags) } -func hydrateSetSchema(sch *schema.Schema, name string, ty cty.Type, defaults *typeexpr.Defaults) error { +func hydrateSetSchema(sch *schema.Schema, name string, ty cty.Type, defaults *typeexpr.Defaults, diags []result.Diagnostic) []result.Diagnostic { sch.UniqueItems = true - return hydrateArraySchema(sch, name, ty, defaults) + return hydrateArraySchema(sch, name, ty, defaults, diags) +} + +func hydrateAnySchema(sch *schema.Schema, diags []result.Diagnostic) []result.Diagnostic { + sch.Comment = "Airlock warning: unconstrained type from OpenTofu/Terraform 'any'" + return append(diags, result.Diagnostic{ + Path: sch.Title, + Code: "unconstrained_type", + Message: fmt.Sprintf("unconstrained type in field '%s' from OpenTofu/Terraform 'any'", sch.Title), + Level: result.Warning, + }) } func ctyValueToInterface(val cty.Value) interface{} { diff --git a/pkg/opentofu/tofutoschema_test.go b/pkg/opentofu/tofutoschema_test.go index 47ccae4..c19822c 100644 --- a/pkg/opentofu/tofutoschema_test.go +++ b/pkg/opentofu/tofutoschema_test.go @@ -4,65 +4,66 @@ import ( "encoding/json" "os" "path/filepath" - "strings" "testing" "github.com/massdriver-cloud/airlock/pkg/opentofu" + "github.com/massdriver-cloud/airlock/pkg/result" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestTofuToSchema(t *testing.T) { type testData struct { - name string - err string + name string + diags []result.Diagnostic } tests := []testData{ { name: "simple", - }, - { - name: "any", - err: "type 'any' cannot be converted to a JSON schema type", - }, - { - name: "nestedany", - err: "dynamic types are not supported (are you using type 'any'?)", - }, - { - name: "empty", - err: "type cannot be empty", + diags: []result.Diagnostic{ + { + Path: "any", + Code: "unconstrained_type", + Message: "unconstrained type in field 'any' from OpenTofu/Terraform 'any'", + Level: result.Warning, + }, + { + Path: "foo", + Code: "unconstrained_type", + Message: "unconstrained type in field 'foo' from OpenTofu/Terraform 'any'", + Level: result.Warning, + }, + { + Path: "empty", + Code: "unconstrained_type", + Message: "unconstrained type in field 'empty' from OpenTofu/Terraform 'any'", + Level: result.Warning, + }, + }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { modulePath := filepath.Join("testdata/opentofu", tc.name) - got, schemaErr := opentofu.TofuToSchema(modulePath) - if schemaErr != nil && tc.err == "" { - t.Fatalf("unexpected error: %s", schemaErr.Error()) - } - if tc.err != "" && schemaErr == nil { - t.Fatalf("expected error %s, got nil", tc.err) - } - if tc.err != "" && !strings.Contains(schemaErr.Error(), tc.err) { - t.Fatalf("expected error %s, got %s", tc.err, schemaErr.Error()) - } - if tc.err != "" { - return - } + got := opentofu.TofuToSchema(modulePath) - bytes, marshalErr := json.Marshal(got) + gotSchema, marshalErr := json.Marshal(got.Schema) if marshalErr != nil { t.Fatalf("unexpected error: %s", marshalErr.Error()) } - want, readErr := os.ReadFile(filepath.Join("testdata/opentofu", tc.name, "schema.json")) + wantSchema, readErr := os.ReadFile(filepath.Join("testdata/opentofu", tc.name, "schema.json")) if readErr != nil { t.Fatalf("unexpected error: %s", readErr.Error()) } - require.JSONEq(t, string(want), string(bytes)) + require.JSONEq(t, string(wantSchema), string(gotSchema)) + + gotDiags := got.Diags + + assert.ElementsMatch(t, tc.diags, gotDiags) }) } } diff --git a/pkg/prettylogs/main.go b/pkg/prettylogs/main.go index 974cb7c..9f6fb13 100644 --- a/pkg/prettylogs/main.go +++ b/pkg/prettylogs/main.go @@ -13,3 +13,7 @@ func Green(word string) lipgloss.Style { func Orange(word string) lipgloss.Style { return lipgloss.NewStyle().SetString(word).Foreground(lipgloss.Color("#FFA500")) } + +func Red(word string) lipgloss.Style { + return lipgloss.NewStyle().SetString(word).Foreground(lipgloss.Color("#FF0000")) +} diff --git a/pkg/result/print.go b/pkg/result/print.go new file mode 100644 index 0000000..edd0fa9 --- /dev/null +++ b/pkg/result/print.go @@ -0,0 +1,31 @@ +package result + +import ( + "encoding/json" + "fmt" + + "github.com/massdriver-cloud/airlock/pkg/prettylogs" +) + +func (result *SchemaResult) PrettyDiags() string { + output := "" + for _, diag := range result.Diags { + levelString := prettylogs.Orange("WARNING") + if diag.Level == Error { + levelString = prettylogs.Red("ERROR") + } + output += fmt.Sprintf("Airlock %s: %s\n", levelString, diag.Message) + } + return output +} + +func (result *SchemaResult) PrettySchema() string { + if result.Schema == nil { + return "No schema available" + } + bytes, err := json.MarshalIndent(result.Schema, "", " ") + if err != nil { + return fmt.Sprintf("Error marshaling schema: %s", err) + } + return string(bytes) +} diff --git a/pkg/result/types.go b/pkg/result/types.go new file mode 100644 index 0000000..ef900f5 --- /dev/null +++ b/pkg/result/types.go @@ -0,0 +1,27 @@ +package result + +import "github.com/massdriver-cloud/airlock/pkg/schema" + +type SchemaResult struct { + Schema *schema.Schema + Diags []Diagnostic +} + +type CodeResult struct { + Code []byte + Diags []Diagnostic +} + +type Severity string + +const ( + Warning Severity = "warning" + Error Severity = "error" +) + +type Diagnostic struct { + Path string + Code string + Message string + Level Severity +} diff --git a/pkg/schema/types.go b/pkg/schema/types.go index d4ceb63..9e88bc3 100644 --- a/pkg/schema/types.go +++ b/pkg/schema/types.go @@ -16,7 +16,7 @@ type Schema struct { Ref string `json:"$ref,omitempty"` // section 8.2.3.1 DynamicRef string `json:"$dynamicRef,omitempty"` // section 8.2.3.2 // Definitions Definitions `json:"$defs,omitempty"` // section 8.2.4 - Comments string `json:"$comment,omitempty"` // section 8.3 + Comment string `json:"$comment,omitempty"` // section 8.3 // RFC draft-bhutton-json-schema-00 section 10.2.1 (Sub-schemas with logic) AllOf []*Schema `json:"allOf,omitempty"` // section 10.2.1.1 AnyOf []*Schema `json:"anyOf,omitempty"` // section 10.2.1.2