Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ jobs:
with:
go-version: "1.25"

- name: Set up pkl
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unit tests shouldn't rely on PKL being present - that should be mocked so the tests are deterministic

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok we can do this. But I'll have to extract out the pkl tests to their own function, deviating from the existing pattern of putting tuples in the testData array

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine - it's the first time we have to rely on the presence of a local binary

Copy link
Collaborator

@kehoecj kehoecj Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@akhil-rasheed Looking at the PKL Go documentation, I do not think the binary is required anymore. The PKL binary is only required when using code generation and it looks like you can load PKL into a struct without using Code Generation: https://pkl-lang.org/go/current/evaluation.html#without-code-generation. This may only apply to a pre-defined struct though which wouldn't work.

Please take a look and see if this is a viable path. This would greatly simplify the implementation.

uses: pkl-community/setup-pkl@5bb6ac805eb51c448837ec34e9957a18adab927d # v0.0.5
with:
pkl-version: '0.26.3'

- name: Unit test
run: go test -v -cover -coverprofile coverage.out ./...

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
* INI
* JSON
* Properties
* PKL _(requires that the `pkl` binary is installed; `.pkl` files will be ignored if the binary is not installed)_
* TOML
* XML
* YAML
Expand Down Expand Up @@ -187,8 +188,7 @@ validator --exclude-dirs=/path/to/search/tests /path/to/search
![Exclude Dirs Run](./img/exclude_dirs.gif)

#### Exclude file types

Exclude file types in the search path. Available file types are `csv`, `env`, `hcl`, `hocon`, `ini`, `json`, `plist`, `properties`, `toml`, `xml`, `yaml`, and `yml`
Exclude file types in the search path. Available file types are `csv`, `env`, `hcl`, `hocon`, `ini`, `json`, `plist`, `properties`, `pkl`, `toml`, `xml`, `yaml`, and `yml`

```shell
validator --exclude-file-types=json /path/to/search
Expand Down
2 changes: 1 addition & 1 deletion cmd/validator/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, XML, and YAML.
Currently Apple PList XML, CSV, HCL, HOCON, INI, JSON, PKL, Properties, TOML, XML, and YAML.
configuration file types are supported.

Usage: validator [OPTIONS] [<search_path>...]
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ require (
github.com/agext/levenshtein v1.2.1 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/apple/pkl-go v0.8.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
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
Expand Down
15 changes: 15 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -36,6 +38,19 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
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/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/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/zclconf/go-cty v1.13.0 h1:It5dfKTTZHe9aeppbNOda3mN7Ag7sg6QkBNm6TkyFa0=
Expand Down
4 changes: 2 additions & 2 deletions index.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
* INI
* JSON
* Properties
* PKL _(requires that the `pkl` binary is installed; `.pkl` files will be ignored if the binary is not installed)_
* TOML
* XML
* YAML
Expand Down Expand Up @@ -200,8 +201,7 @@ validator --exclude-dirs=/path/to/search/tests /path/to/search
![Exclude Dirs Run](./img/exclude_dirs.gif)

#### Exclude file types

Exclude file types in the search path. Available file types are `csv`, `env`, `hcl`, `hocon`, `ini`, `json`, `plist`, `properties`, `toml`, `xml`, `yaml`, and `yml`
Exclude file types in the search path. Available file types are `csv`, `env`, `hcl`, `hocon`, `ini`, `json`, `plist`, `properties`, `pkl`, `toml`, `xml`, `yaml`, and `yml`

```shell
validator --exclude-file-types=json /path/to/search
Expand Down
7 changes: 7 additions & 0 deletions pkg/cli/cli.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package cli

import (
"errors"
"fmt"
"os"

"github.com/Boeing/config-file-validator/pkg/finder"
"github.com/Boeing/config-file-validator/pkg/reporter"
"github.com/Boeing/config-file-validator/pkg/validator"
)

// GroupOutput is a global variable that is used to
Expand Down Expand Up @@ -95,6 +97,11 @@ func (c CLI) Run() (int, error) {
}

isValid, err := fileToValidate.FileType.Validator.Validate(fileContent)
if errors.Is(err, validator.ErrPklSkipped) {
fmt.Printf("Warning: 'pkl' binary not found, file %s will be ignored.\n", fileToValidate.Path)
continue
}

if !isValid {
errorFound = true
}
Expand Down
9 changes: 9 additions & 0 deletions pkg/filetype/file_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -120,6 +128,7 @@ var FileTypes = []FileType{
TomlFileType,
IniFileType,
PropFileType,
PklFileType,
HclFileType,
PlistFileType,
CsvFileType,
Expand Down
51 changes: 51 additions & 0 deletions pkg/validator/pkl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package validator

import (
"context"
"errors"
"fmt"
"os/exec"

"github.com/apple/pkl-go/pkl"
)

var (
// ErrPklSkipped is returned when a validation is skipped due to a missing dependency.
ErrPklSkipped = errors.New("validation skipped")
)

Check failure on line 15 in pkg/validator/pkl.go

View workflow job for this annotation

GitHub Actions / lint (1.25, ubuntu-latest)

File is not properly formatted (gci)

Check failure on line 15 in pkg/validator/pkl.go

View workflow job for this annotation

GitHub Actions / lint (1.25, macos-latest)

File is not properly formatted (gci)
// 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)
}

// Validate attempts to evaluate the provided byte slice as a PKL file.
// If the 'pkl' binary is not found, it returns ErrSkipped.
func (v PklValidator) Validate(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, pkl.PreconfiguredOptions)
if err != nil {
// If the error is that the pkl binary was not found, return ErrPklSkipped.
var execErr *exec.Error
if errors.As(err, &execErr) && execErr.Err == exec.ErrNotFound {

Check failure on line 39 in pkg/validator/pkl.go

View workflow job for this annotation

GitHub Actions / lint (1.25, ubuntu-latest)

comparing with == will fail on wrapped errors. Use errors.Is to check for a specific error (errorlint)

Check failure on line 39 in pkg/validator/pkl.go

View workflow job for this annotation

GitHub Actions / lint (1.25, macos-latest)

comparing with == will fail on wrapped errors. Use errors.Is to check for a specific error (errorlint)
return false, ErrPklSkipped
}
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
}
49 changes: 49 additions & 0 deletions pkg/validator/validator_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package validator

import (
"context"
_ "embed"
"errors"
"os/exec"
"strings"
"testing"

"github.com/apple/pkl-go/pkl"
)

var (
Expand Down Expand Up @@ -55,6 +61,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{}},
Expand All @@ -80,6 +88,14 @@ func Test_ValidationInput(t *testing.T) {
t.Parallel()

valid, err := tcase.validator.Validate(tcase.testInput)

// If the validator is PklValidator and it returns ErrSkipped, skip the test.
if _, ok := tcase.validator.(PklValidator); ok {
if errors.Is(err, ErrPklSkipped) {
t.Skip("Skipping test: 'pkl' binary not found.")
}
}

if valid != tcase.expectedResult {
t.Errorf("incorrect result: expected %v, got %v", tcase.expectedResult, valid)
}
Expand All @@ -94,3 +110,36 @@ func Test_ValidationInput(t *testing.T) {
})
}
}

func TestPklValidator_BinaryMissing(t *testing.T) {
validator := PklValidator{
evaluatorFactory: func(_ context.Context, _ ...func(*pkl.EvaluatorOptions)) (pkl.Evaluator, error) {
return nil, &exec.Error{Err: exec.ErrNotFound}
},
}
_, err := validator.Validate([]byte(`name = "test"`))

if !errors.Is(err, ErrPklSkipped) {
t.Errorf("expected ErrPklSkipped, got %v", err)
}
}

func TestPklValidator_EvaluatorCreationError(t *testing.T) {
expectedErr := errors.New("evaluator creation failed")

validator := PklValidator{
evaluatorFactory: func(_ context.Context, _ ...func(*pkl.EvaluatorOptions)) (pkl.Evaluator, error) {
return nil, expectedErr
},
}

_, err := validator.Validate([]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)
}
}
50 changes: 50 additions & 0 deletions test/fixtures/good.pkl
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@


name = "PhoenixWebApp"

package {
name = "phoenix"
version = "2.1.0"
authors = List("Phoenix Engineering <[email protected]>")
}

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
}
)
1 change: 1 addition & 0 deletions test/fixtures/subdir2/bad.pkl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"invalid"
Loading