Skip to content

Commit ad114f9

Browse files
authored
Add wpm.json validator (#8)
* Add go-playground/validator/v10 * Add wpm.json validator * Add package validator instance in cli state * Update dev dependencies key in validator * Fix package validator instance * Remove regex from package name validation * Add validation errors interface * Update validation error interface * Add package fields validation * Add package field description and error handling for package validator * Add error check on adding validator validation methods
1 parent 6242405 commit ad114f9

File tree

6 files changed

+186
-19
lines changed

6 files changed

+186
-19
lines changed

cli/command/cli.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import (
1111
"wpm/cli/version"
1212
"wpm/pkg/config"
1313
"wpm/pkg/config/configfile"
14+
"wpm/pkg/validator"
1415

16+
goValidator "github.com/go-playground/validator/v10"
1517
"github.com/spf13/cobra"
1618
)
1719

@@ -30,6 +32,7 @@ type Cli interface {
3032
Apply(ops ...CLIOption) error
3133
ConfigFile() *configfile.ConfigFile
3234
RegistryClient() (client.RegistryClient, error)
35+
PackageValidator() (*goValidator.Validate, error)
3336
}
3437

3538
// WpmCli is an instance the wpm command line client.
@@ -148,6 +151,11 @@ func (cli *WpmCli) RegistryClient() (client.RegistryClient, error) {
148151
return _client, nil
149152
}
150153

154+
// PackageValidator returns a new instance of the package validator
155+
func (cli *WpmCli) PackageValidator() (*goValidator.Validate, error) {
156+
return validator.NewValidator()
157+
}
158+
151159
// UserAgent returns the user agent string used for making API requests
152160
func UserAgent() string {
153161
return "wpm-cli/" + version.Version + " (" + runtime.GOOS + "/" + runtime.GOARCH + ")"

cli/command/init/init.go

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os"
88
"path/filepath"
99
"wpm/cli/command"
10+
"wpm/pkg/validator"
1011

1112
"github.com/pkg/errors"
1213
"github.com/spf13/cobra"
@@ -25,18 +26,6 @@ type initOptions struct {
2526
yes bool
2627
}
2728

28-
type PackageInfo struct {
29-
Name string `json:"name"`
30-
Version string `json:"version"`
31-
License string `json:"license"`
32-
Type string `json:"type"`
33-
Tags []string `json:"tags"`
34-
Platform struct {
35-
PHP string `json:"php"`
36-
WP string `json:"wp"`
37-
} `json:"platform"`
38-
}
39-
4029
type prompt struct {
4130
Msg string
4231
Default string
@@ -77,16 +66,13 @@ func runInit(ctx context.Context, wpmCli command.Cli, opts initOptions) error {
7766
}
7867

7968
basecwd := filepath.Base(cwd)
80-
wpmJsonInitData := PackageInfo{
69+
wpmJsonInitData := validator.Package{
8170
Name: basecwd,
8271
Version: defaultVersion,
8372
License: defaultLicense,
8473
Type: defaultType,
8574
Tags: []string{},
86-
Platform: struct {
87-
PHP string `json:"php"`
88-
WP string `json:"wp"`
89-
}{
75+
Platform: validator.PackagePlatform{
9076
PHP: defaultPHP,
9177
WP: defaultWP,
9278
},
@@ -129,14 +115,23 @@ func runInit(ctx context.Context, wpmCli command.Cli, opts initOptions) error {
129115
}
130116
}
131117

118+
ve, err := wpmCli.PackageValidator()
119+
if err != nil {
120+
return err
121+
}
122+
123+
if err := validator.ValidatePackage(wpmJsonInitData, ve); err != nil {
124+
return err
125+
}
126+
132127
if err := writeWpmJson(wpmCli, wpmJsonPath, wpmJsonInitData); err != nil {
133128
return err
134129
}
135130

136131
return nil
137132
}
138133

139-
func writeWpmJson(wpmCli command.Cli, path string, data PackageInfo) error {
134+
func writeWpmJson(wpmCli command.Cli, path string, data validator.Package) error {
140135
file, err := os.Create(path)
141136
if err != nil {
142137
return err

go.mod

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.23.4
55
require (
66
github.com/docker/docker v27.4.1+incompatible
77
github.com/fvbommel/sortorder v1.1.0
8+
github.com/go-playground/validator/v10 v10.23.0
89
github.com/henvic/httpretty v0.1.4
910
github.com/moby/term v0.5.0
1011
github.com/morikuni/aec v1.0.0
@@ -19,5 +20,11 @@ require (
1920

2021
require (
2122
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
23+
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
24+
github.com/go-playground/locales v0.14.1 // indirect
25+
github.com/go-playground/universal-translator v0.18.1 // indirect
2226
github.com/inconshreveable/mousetrap v1.1.0 // indirect
27+
github.com/leodido/go-urn v1.4.0 // indirect
28+
golang.org/x/crypto v0.19.0 // indirect
29+
golang.org/x/net v0.21.0 // indirect
2330
)

go.sum

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,22 @@ github.com/docker/docker v27.4.1+incompatible h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWD
1010
github.com/docker/docker v27.4.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
1111
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
1212
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
13+
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
14+
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
15+
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
16+
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
17+
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
18+
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
19+
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
20+
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
21+
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
22+
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
1323
github.com/henvic/httpretty v0.1.4 h1:Jo7uwIRWVFxkqOnErcoYfH90o3ddQyVrSANeS4cxYmU=
1424
github.com/henvic/httpretty v0.1.4/go.mod h1:Dn60sQTZfbt2dYsdUSNsCljyF4AfdqnuJFDLJA1I4AM=
1525
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
1626
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
27+
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
28+
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
1729
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
1830
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
1931
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
@@ -30,10 +42,15 @@ github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3k
3042
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
3143
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
3244
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
33-
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
3445
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
46+
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
47+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
3548
github.com/thlib/go-timezone-local v0.0.6 h1:Ii3QJ4FhosL/+eCZl6Hsdr4DDU4tfevNoV83yAEo2tU=
3649
github.com/thlib/go-timezone-local v0.0.6/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI=
50+
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
51+
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
52+
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
53+
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
3754
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
3855
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
3956
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

pkg/validator/errors.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package validator
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
goValidator "github.com/go-playground/validator/v10"
8+
"github.com/morikuni/aec"
9+
)
10+
11+
// ValidatorError represents an error response from the wpm schema validator.
12+
type ValidatorError struct {
13+
Errors []ValidatorErrorItem
14+
}
15+
16+
type ValidatorErrorItem struct {
17+
Message string
18+
FailedField string
19+
}
20+
21+
// Allow ValidatorError to satisfy error interface.
22+
func (err *ValidatorError) Error() string {
23+
// Add all error messages to a string.
24+
message := fmt.Sprintf("\n%s\n", aec.RedF.Apply("config validation failed"))
25+
26+
for _, e := range err.Errors {
27+
if e.FailedField == "DevDependencies" {
28+
e.FailedField = "dev_dependencies"
29+
}
30+
31+
message += fmt.Sprintf(" - %s %s", aec.Bold.Apply(strings.ToLower(e.FailedField)), e.Message)
32+
if e != err.Errors[len(err.Errors)-1] {
33+
message += "\n"
34+
}
35+
}
36+
37+
return message
38+
}
39+
40+
// HandleValidatorError parses validation error into a ValidatorError.
41+
func HandleValidatorError(errs error) error {
42+
validationErrors := &ValidatorError{}
43+
44+
for _, err := range errs.(goValidator.ValidationErrors) {
45+
ve := &ValidatorErrorItem{}
46+
47+
ve.FailedField = err.Field()
48+
ve.Message = PackageFieldDescriptions[err.Field()]
49+
50+
validationErrors.Errors = append(validationErrors.Errors, *ve)
51+
}
52+
53+
return validationErrors
54+
}

pkg/validator/validator.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package validator
2+
3+
import (
4+
"regexp"
5+
6+
goValidator "github.com/go-playground/validator/v10"
7+
)
8+
9+
// Platform struct to define the platform field
10+
type PackagePlatform struct {
11+
WP string `json:"wp" validate:"required"`
12+
PHP string `json:"php" validate:"required"`
13+
}
14+
15+
// Package struct to define the wpm.json schema
16+
type Package struct {
17+
Name string `json:"name" validate:"required,min=3,max=164"`
18+
Description string `json:"description,omitempty"`
19+
Type string `json:"type" validate:"required,oneof=plugin theme mu-plugin drop-in"`
20+
Version string `json:"version" validate:"required,semver,max=64"`
21+
License string `json:"license" validate:"omitempty"`
22+
Homepage string `json:"homepage,omitempty" validate:"omitempty,url"`
23+
Tags []string `json:"tags,omitempty" validate:"dive,max=5"`
24+
Team []string `json:"team,omitempty"`
25+
Bin map[string]string `json:"bin,omitempty"`
26+
Platform PackagePlatform `json:"platform" validate:"required"`
27+
Dependencies map[string]string `json:"dependencies,omitempty"`
28+
DevDependencies map[string]string `json:"dev_dependencies,omitempty"`
29+
Scripts map[string]string `json:"scripts,omitempty"`
30+
}
31+
32+
// Description of package fields.
33+
var PackageFieldDescriptions = map[string]string{
34+
"Name": "must contain only lowercase letters, numbers, and hyphens, and be between 3 and 164 characters. (required)",
35+
"Description": "should be a string. (optional)",
36+
"Type": "must be one of: 'plugin', 'theme', 'mu-plugin', or 'drop-in'. (required)",
37+
"Version": "must be a valid semantic version (semver) and less than 64 characters. (required)",
38+
"License": "must be a string. (optional)",
39+
"Homepage": "must be a valid url. (optional)",
40+
"Tags": "must be an array of strings with a maximum of 5 tags. (optional)",
41+
"Team": "must be an array of strings. (optional)",
42+
"Bin": "must be an object with string values. (optional)",
43+
"Platform": "must contain wp and php versions. (required)",
44+
"Dependencies": "must be an object with string values. (optional)",
45+
"DevDependencies": "must be an object with string values. (optional)",
46+
"Scripts": "must be an object with string values. (optional)",
47+
}
48+
49+
// Dist struct to define the dist field
50+
type PackageDist struct {
51+
Size int `json:"size" validate:"gte=0"`
52+
FileCount int `json:"fileCount" validate:"gte=0"`
53+
Digest string `json:"digest" validate:"required,sha256"`
54+
}
55+
56+
// NewValidator creates a new validator instance.
57+
func NewValidator() (*goValidator.Validate, error) {
58+
validator := goValidator.New()
59+
err := validator.RegisterValidation("package_name_regex", packageNameRegex)
60+
if err != nil {
61+
return nil, err
62+
}
63+
64+
return validator, nil
65+
}
66+
67+
// ValidatePackage validates the package struct.
68+
func ValidatePackage(pkg Package, v *goValidator.Validate) error {
69+
errs := v.Struct(pkg)
70+
if errs != nil {
71+
return HandleValidatorError(errs)
72+
}
73+
74+
return nil
75+
}
76+
77+
// packageNameRegex validates the package name field with a regex.
78+
// Only lowercase letters, numbers, and hyphens are allowed.
79+
func packageNameRegex(fl goValidator.FieldLevel) bool {
80+
value := fl.Field().String()
81+
if value == "" {
82+
return false
83+
}
84+
85+
return regexp.MustCompile(`^[a-z0-9-]+$`).MatchString(value)
86+
}

0 commit comments

Comments
 (0)