diff --git a/README.md b/README.md index f8b26ab4..542c580f 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ * HOCON * INI * JSON +* PKL * Properties * TOML * TOON @@ -197,7 +198,7 @@ validator --exclude-dirs=/path/to/search/tests /path/to/search #### Exclude file types -Exclude file types in the search path. Available file types are `csv`, `env`, `hcl`, `hocon`, `ini`, `json`, `plist`, `properties`, `toml`, `toon`, `xml`, `yaml`, and `yml` +Exclude file types in the search path. Available file types are `csv`, `env`, `hcl`, `hocon`, `ini`, `json`, `pkl`, `plist`, `properties`, `toml`, `toon`, `xml`, `yaml`, and `yml` ```shell validator --exclude-file-types=json /path/to/search diff --git a/cmd/validator/validator.go b/cmd/validator/validator.go index 04d26f00..0c2ad148 100644 --- a/cmd/validator/validator.go +++ b/cmd/validator/validator.go @@ -2,7 +2,7 @@ Validator recursively scans a directory to search for configuration files and validates them using the go package for each configuration type. -Currently Apple PList XML, CSV, HCL, HOCON, INI, JSON, Properties, TOML, TOON, XML, and YAML. +Currently Apple PList XML, CSV, HCL, HOCON, INI, JSON, PKL, Properties, TOML, TOON, XML, and YAML. configuration file types are supported. Usage: validator [OPTIONS] [...] diff --git a/go.mod b/go.mod index 1bab9091..bb34c1a5 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/Boeing/config-file-validator go 1.25.0 require ( + github.com/apple/pkl-go v0.8.0 github.com/bmatcuk/doublestar/v4 v4.9.1 github.com/editorconfig/editorconfig-core-go/v2 v2.6.2 github.com/fatih/color v1.18.0 @@ -27,6 +28,8 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/zclconf/go-cty v1.13.0 // indirect golang.org/x/mod v0.16.0 // indirect golang.org/x/sys v0.25.0 // indirect diff --git a/go.sum b/go.sum index ce96a7c7..ff7e5e58 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6 github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/apple/pkl-go v0.8.0 h1:GRcBvFWeXjT9rc7A5gHK89qrel2wGZ3/a7ge4rPlT5M= +github.com/apple/pkl-go v0.8.0/go.mod h1:5Hwil5tyZGrOekh7JXLZJvIAcGHb4gT19lnv4WEiKeI= github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -38,6 +40,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/toon-format/toon-go v0.0.0-20251108125615-44b4cd22477f h1:qIMJqAPGPH7S4uVRaHflMfJ/ZenGp7W1tWECmVoJitM= github.com/toon-format/toon-go v0.0.0-20251108125615-44b4cd22477f/go.mod h1:j/BOnpF2ihnz4lELs99h9mwGJBx/zdleOUCnLLRPCsc= github.com/zclconf/go-cty v1.13.0 h1:It5dfKTTZHe9aeppbNOda3mN7Ag7sg6QkBNm6TkyFa0= diff --git a/index.md b/index.md index abbd6dec..30ab02ab 100644 --- a/index.md +++ b/index.md @@ -61,6 +61,7 @@ * INI * JSON * Properties +* PKL * TOML * TOON * XML @@ -208,7 +209,7 @@ validator --exclude-dirs=/path/to/search/tests /path/to/search #### Exclude file types -Exclude file types in the search path. Available file types are `csv`, `env`, `hcl`, `hocon`, `ini`, `json`, `plist`, `properties`, `toml`, `toon`, `xml`, `yaml`, and `yml` +Exclude file types in the search path. Available file types are `csv`, `env`, `hcl`, `hocon`, `ini`, `json`, `pkl`, `plist`, `properties`, `toml`, `toon`, `xml`, `yaml`, and `yml` ```shell validator --exclude-file-types=json /path/to/search diff --git a/pkg/filetype/file_type.go b/pkg/filetype/file_type.go index b476764f..cd174804 100644 --- a/pkg/filetype/file_type.go +++ b/pkg/filetype/file_type.go @@ -63,6 +63,14 @@ var PropFileType = FileType{ validator.PropValidator{}, } +// Instance of the FileType object to +// represent a Pkl file +var PklFileType = FileType{ + "pkl", + tools.ArrToMap("pkl"), + validator.PklValidator{}, +} + // Instance of the FileType object to // represent a HCL file var HclFileType = FileType{ @@ -128,6 +136,7 @@ var FileTypes = []FileType{ TomlFileType, IniFileType, PropFileType, + PklFileType, HclFileType, PlistFileType, CsvFileType, diff --git a/pkg/validator/pkl.go b/pkg/validator/pkl.go new file mode 100644 index 00000000..90fdb3c1 --- /dev/null +++ b/pkg/validator/pkl.go @@ -0,0 +1,51 @@ +package validator + +import ( + "context" + "fmt" + + "github.com/apple/pkl-go/pkl" +) + +// PklValidator is used to validate a byte slice that is intended to represent a +// PKL file. +type PklValidator struct { + evaluatorFactory func(context.Context, ...func(*pkl.EvaluatorOptions)) (pkl.Evaluator, error) +} + +// ValidateSyntax attempts to evaluate the provided byte slice as a PKL file. +func (v PklValidator) ValidateSyntax(b []byte) (bool, error) { + ctx := context.Background() + + // Convert the byte slice to a ModuleSource using TextSource + source := pkl.TextSource(string(b)) + + evaluatorFactory := v.evaluatorFactory + if evaluatorFactory == nil { + evaluatorFactory = pkl.NewEvaluator + } + + evaluator, err := evaluatorFactory(ctx) + if err != nil { + return false, fmt.Errorf("failed to create evaluator: %w", err) + } + + _, err = evaluator.EvaluateExpressionRaw(ctx, source, "") + if err != nil { + return false, fmt.Errorf("failed to evaluate module: %w", err) + } + + return true, nil +} + +// ValidateFormat is not yet implemented for PklValidator. +func (v PklValidator) ValidateFormat(b []byte, options any) (bool, error) { + if options == nil { + // If no specific format options are provided, consider it valid for now. + // A more robust implementation would involve Pkl schema validation. + return true, nil + } + // If options are provided, it means a specific format validation is requested, + // which is not yet implemented. + return false, ErrMethodUnimplemented +} diff --git a/pkg/validator/validator_test.go b/pkg/validator/validator_test.go index 723fa286..00ebdb83 100644 --- a/pkg/validator/validator_test.go +++ b/pkg/validator/validator_test.go @@ -1,8 +1,13 @@ package validator import ( + "context" _ "embed" + "errors" + "strings" "testing" + + "github.com/apple/pkl-go/pkl" ) var ( @@ -71,6 +76,8 @@ var testData = []struct { {"invalidIni", []byte(`\nCatalog hidden\n`), false, IniValidator{}}, {"validProperties", []byte("key=value\nkey2=${key}"), true, PropValidator{}}, {"invalidProperties", []byte("key=${key}"), false, PropValidator{}}, + {"validPkl", []byte(`name = "Swallow"`), true, PklValidator{}}, + {"invalidPkl", []byte(`"name" = "Swallow"`), false, PklValidator{}}, {"validHcl", []byte(`key = "value"`), true, HclValidator{}}, {"invalidHcl", []byte(`"key" = "value"`), false, HclValidator{}}, {"multipleInvalidHcl", []byte(`"key1" = "value1"\n"key2"="value2"`), false, HclValidator{}}, @@ -111,6 +118,25 @@ func Test_ValidationInput(t *testing.T) { } } +func TestPklValidator_EvaluatorCreationError(t *testing.T) { + expectedErr := errors.New("evaluator creation failed") + + validator := PklValidator{ + evaluatorFactory: func(_ context.Context, _ ...func(options *pkl.EvaluatorOptions)) (pkl.Evaluator, error) { + return nil, expectedErr + }, + } + + _, err := validator.ValidateSyntax([]byte(`name = "test"`)) + + if !strings.Contains(err.Error(), "failed to create evaluator") { + t.Errorf("expected error to contain 'failed to create evaluator', got %v", err) + } + + if !errors.Is(err, expectedErr) { + t.Errorf("expected error to wrap %v, got %v", expectedErr, err) + } +} func addFuzzCases(f *testing.F) { f.Helper() for _, tc := range fuzzbank { diff --git a/test/fixtures/good.pkl b/test/fixtures/good.pkl new file mode 100644 index 00000000..5f72357d --- /dev/null +++ b/test/fixtures/good.pkl @@ -0,0 +1,50 @@ + + +name = "PhoenixWebApp" + +package { + name = "phoenix" + version = "2.1.0" + authors = List("Phoenix Engineering ") +} + +database { + host = "db.phoenix.internal" + port = 5432 + username = "phoenix_user" + poolSize = 20 + sslMode = "require" +} + +server { + host = "0.0.0.0" + port = 8080 + logLevel = "info" // "debug", "info", "warn", "error" + corsOrigins = List("https://phoenix.example", "https://app.phoenix.example") +} + +features { + newSignupFlow = true + apiRateLimiting = true + dashboardAnalytics = false +} + +// --- Service Replica Configuration --- +local class Replica { + region: "us-east-1" | "us-west-2" | "eu-central-1" + instanceType: String + count: Int +} + +replicas = List( + new Replica { + region = "us-east-1" + instanceType = "t3.medium" + count = 3 + }, + new Replica { + region = "us-west-2" + instanceType = "t3.medium" + count = 2 + } +) diff --git a/test/fixtures/subdir2/bad.pkl b/test/fixtures/subdir2/bad.pkl new file mode 100644 index 00000000..50a269a4 --- /dev/null +++ b/test/fixtures/subdir2/bad.pkl @@ -0,0 +1 @@ +"invalid"