Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Golang library for validating package against specs #10

Closed
wants to merge 32 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
9ba6f42
Stubbing out golang library API
ycombinator Jul 29, 2020
7c77ab4
Ignoring .idea folder
ycombinator Jul 29, 2020
a08155a
Adding Makefile
ycombinator Jul 29, 2020
534caaa
Adding spec to golang lib
ycombinator Jul 29, 2020
3379d67
Adding check target
ycombinator Jul 29, 2020
fa6ccf8
Making Makefile more generic
ycombinator Jul 29, 2020
8bed0d7
Creating stub code in internal/
ycombinator Jul 29, 2020
aef591a
Fleshing out
ycombinator Jul 30, 2020
5b717dc
Adding Makefile build target
ycombinator Jul 30, 2020
73d4073
Running go mod tidy
ycombinator Jul 30, 2020
10fb52e
Adding validation error tests
ycombinator Jul 30, 2020
599a0dd
Updating API
ycombinator Jul 30, 2020
777fcfa
Updating README
ycombinator Jul 30, 2020
fbb6b57
Minor linting
ycombinator Jul 30, 2020
08e8467
Add recipe
ycombinator Jul 30, 2020
651fa65
Add test target to Makefile
ycombinator Jul 30, 2020
30fce3a
Adding package tests
ycombinator Jul 30, 2020
17036c0
Consolidating tests
ycombinator Jul 31, 2020
fbf6bb0
Adding spec tests
ycombinator Jul 31, 2020
81d8b6d
Fleshing out validation logic a bit
ycombinator Jul 31, 2020
7dfd578
Running go mod tidy
ycombinator Jul 31, 2020
a714978
Adding TODOs
ycombinator Jul 31, 2020
894c832
Adding top-level test Makefile target
ycombinator Jul 31, 2020
61df88e
Updating README
ycombinator Jul 31, 2020
dcb6768
Cleaning out bundled spec folder before copying over latest specs
ycombinator Jul 31, 2020
43a18db
Updating bundled specs
ycombinator Jul 31, 2020
f18eadd
Fleshing out folder validation a bit
ycombinator Aug 5, 2020
181440a
Adding inverse validation
ycombinator Aug 5, 2020
03ba766
Fleshing out TODO
ycombinator Aug 5, 2020
b5d71d4
Updating specs
ycombinator Aug 6, 2020
a3a3ae4
Removing unnecessary method
ycombinator Aug 6, 2020
fcd9793
More validation logic for sub-folders
ycombinator Aug 6, 2020
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea/
11 changes: 11 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Updates the spec in language libraries
update: code/*
$(foreach lang,$^,make -C $(lang) update;)

# Checks that language libraries have latest specs
check: code/*
$(foreach lang,$^,make -C $(lang) check;)

# Tests the language libraries' code
test: code/*
$(foreach lang,$^,make -C $(lang) test;)
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
# Introduction

This repository contains the specifications for Elastic Packages, as served up by the [Elastic Package Registry (EPR)](https://github.com/elastic/package-registry).
This repository contains:
* specifications for Elastic Packages, as served up by the [Elastic Package Registry (EPR)](https://github.com/elastic/package-registry). There may be multiple versions of the specifications; these can be found under the `versions` top-level folder. Read more in the _Specification Versioning_ section below.
* code libraries for validating said specifications; these can be found under the `code` top-level folder.

| :warning: **WARNING** :warning: |
| ----- |
| The specifications in this repository are currently under active development. They are **NOT** ready for general use. |

In the future it may also contain code for validating said specifications.

# Purpose

Please use this repository to discuss any changes to the specification, either my making issues or PRs to the specification.

# Specification Format
# Specification Format

An Elastic Package specification describes:
1. the folder structure of packages and expected files within these folders; and
Expand Down
18 changes: 18 additions & 0 deletions code/go/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
SPEC_DIR=resources/spec

# Updates the spec to the latest copy
update:
rm -rf ${SPEC_DIR}/*
cp -r ../../versions ${SPEC_DIR}

# Checks that the spec is up-to-date
check:
diff -qr ../../versions ${SPEC_DIR}/versions

# Builds the go module and installs it
build:
go get github.com/elastic/package-spec/code/go

# Runs tests
test:
go test -v ./...
10 changes: 10 additions & 0 deletions code/go/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module github.com/elastic/package-spec/code/go

go 1.14

require (
github.com/Masterminds/semver/v3 v3.1.0
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.6.1
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
)
15 changes: 15 additions & 0 deletions code/go/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk=
github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
27 changes: 27 additions & 0 deletions code/go/internal/validator/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package validator

import (
"fmt"
"strings"
)

// ValidationErrors is an Error that contains a iterable collection of validation error messages.
type ValidationErrors []error

func (ve ValidationErrors) Error() string {
if len(ve) == 0 {
return "found 0 validation errors"
}

var message strings.Builder
errorWord := "errors"
if len(ve) == 1 {
errorWord = "error"
}
fmt.Fprintf(&message, "found %v validation %v:\n", len(ve), errorWord)
for _, err := range ve {
fmt.Fprintf(&message, "\t%v\n", err)
}

return message.String()
}
35 changes: 35 additions & 0 deletions code/go/internal/validator/errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package validator

import (
"errors"
"testing"

"github.com/stretchr/testify/require"
)

func TestValidationErrorsMultiple(t *testing.T) {
ve := ValidationErrors{}
ve = append(ve, errors.New("error 1"))
ve = append(ve, errors.New("error 2"))

require.Len(t, ve, 2)
require.Contains(t, ve.Error(), "found 2 validation errors:")
require.Contains(t, ve.Error(), "error 1")
require.Contains(t, ve.Error(), "error 2")
}

func TestValidationErrorsSingle(t *testing.T) {
ve := ValidationErrors{}
ve = append(ve, errors.New("error 1"))

require.Len(t, ve, 1)
require.Contains(t, ve.Error(), "found 1 validation error:")
require.Contains(t, ve.Error(), "error 1")
}

func TestValidationErrorsNone(t *testing.T) {
ve := ValidationErrors{}

require.Len(t, ve, 0)
require.Contains(t, ve.Error(), "found 0 validation errors")
}
196 changes: 196 additions & 0 deletions code/go/internal/validator/folder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package validator

import (
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"regexp"

"github.com/pkg/errors"
"gopkg.in/yaml.v3"
)

const ItemTypeFile = "file"
const ItemTypeFolder = "folder"

type folderSpec struct {
specPath string
itemSpecs []folderItemSpec
}

type folderItemSpec struct {
Description string `yaml:"description"`
ItemType string `yaml:"type"`
ContentMediaType string `yaml:"contentMediaType"`
Name string `yaml:"name"`
Pattern string `yaml:"pattern"`
Required bool `yaml:"required"`
Ref string `yaml:"$ref"`
Contents []folderItemSpec `yaml:"contents"`
}

func newFolderSpec(specPath string) (*folderSpec, error) {
if _, err := os.Stat(specPath); os.IsNotExist(err) {
return nil, errors.Wrap(err, "no folder specification file found")
}

data, err := ioutil.ReadFile(specPath)
if err != nil {
return nil, errors.Wrap(err, "could not read folder specification file")
}

var wrapper struct {
Spec []folderItemSpec `yaml:",flow"`
}
if err := yaml.Unmarshal(data, &wrapper); err != nil {
return nil, errors.Wrap(err, "could not parse folder specification file")
}

fs := folderSpec{
specPath,
wrapper.Spec,
}

return &fs, nil
}

func (fs *folderSpec) validate(folderPath string) ValidationErrors {
var errs ValidationErrors
files, err := ioutil.ReadDir(folderPath)
if err != nil {
errs = append(errs, errors.Wrapf(err, "could not read folder [%s]", folderPath))
return errs
}

for _, file := range files {
fileName := file.Name()
itemSpec, err := fs.findItemSpec(fileName)
if err != nil {
errs = append(errs, err)
continue
}
if itemSpec == nil {
errs = append(errs, fmt.Errorf("filename [%s] does not match spec for folder [%s]", fileName, folderPath))
continue
}

if file.IsDir() {
if !itemSpec.isSameType(file) {
errs = append(errs, fmt.Errorf("[%s] is a folder but is expected to be a file", fileName))
continue
}

if itemSpec.Ref == "" && itemSpec.Contents == nil {
// No recursive validation needed
continue
}

var subFolderSpec *folderSpec
if itemSpec.Ref != "" {
subFolderSpecPath := path.Join(filepath.Dir(fs.specPath), itemSpec.Ref)
subFolderSpec, err = newFolderSpec(subFolderSpecPath)
if err != nil {
errs = append(errs, err)
continue
}
} else if itemSpec.Contents != nil {
subFolderSpec = &folderSpec{
itemSpecs: itemSpec.Contents,
specPath: fs.specPath,
}
}

subFolderPath := path.Join(folderPath, fileName)
subErrs := subFolderSpec.validate(subFolderPath)
if len(subErrs) > 0 {
errs = append(errs, subErrs)
}

} else {
if !itemSpec.isSameType(file) {
errs = append(errs, fmt.Errorf("[%s] is a file but is expected to be a folder", fileName))
continue
}
// TODO: more validation for file item
}
}

// validate that required items in spec are all accounted for
for _, itemSpec := range fs.itemSpecs {
if !itemSpec.Required {
continue
}

fileFound, err := itemSpec.matchingFileExists(files)
if err != nil {
errs = append(errs, err)
continue
}

if !fileFound {
var err error
if itemSpec.Name != "" {
err = fmt.Errorf("expecting to find %s matching name [%s] in folder [%s]", itemSpec.ItemType, itemSpec.Name, folderPath)
} else if itemSpec.Pattern != "" {
err = fmt.Errorf("expecting to find %s matching pattern [%s] in folder [%s]", itemSpec.ItemType, itemSpec.Pattern, folderPath)
}
errs = append(errs, err)
}
}
return errs
}

func (fs *folderSpec) findItemSpec(folderItemName string) (*folderItemSpec, error) {
for _, itemSpec := range fs.itemSpecs {
if itemSpec.Name != "" && itemSpec.Name == folderItemName {
return &itemSpec, nil
}
if itemSpec.Pattern != "" {
isMatch, err := regexp.MatchString(itemSpec.Pattern, folderItemName)
if err != nil {
return nil, errors.Wrap(err, "invalid folder item spec pattern")
}
if isMatch {
return &itemSpec, nil
}
}
}

// No item spec found
return nil, nil
}

func (is *folderItemSpec) matchingFileExists(files []os.FileInfo) (bool, error) {
if is.Name != "" {
for _, file := range files {
if file.Name() == is.Name {
return is.isSameType(file), nil
}
}
} else if is.Pattern != "" {
for _, file := range files {
isMatch, err := regexp.MatchString(is.Pattern, file.Name())
if err != nil {
return false, errors.Wrap(err, "invalid folder item spec pattern")
}
if isMatch {
return is.isSameType(file), nil
}
}
}

return false, nil
}

func (is *folderItemSpec) isSameType(file os.FileInfo) bool {
switch is.ItemType {
case ItemTypeFile:
return !file.IsDir()
case ItemTypeFolder:
return file.IsDir()
}

return false
}
Loading