From c0bb12840c21c48867d1fe29361439e3f48c2bb2 Mon Sep 17 00:00:00 2001 From: omissis Date: Mon, 8 Aug 2022 22:36:05 +0200 Subject: [PATCH] feat: implement basic file builder functionalities --- go.mod | 4 +- go.sum | 2 + internal/arch/expression.go | 23 ----- internal/arch/file/except/except.go | 26 +++++ internal/arch/file/except/this.go | 7 ++ internal/arch/file/except/this_test.go | 3 + internal/arch/file/rule.go | 96 ++++++++++++++++++ internal/arch/file/rule_test.go | 93 ++++++++++++++++++ internal/arch/file/should/end_with.go | 30 ++++++ internal/arch/file/should/end_with_test.go | 44 +++++++++ internal/arch/file/should/exist.go | 32 ++++++ internal/arch/file/should/exist_test.go | 42 ++++++++ internal/arch/file/should/match_regex.go | 29 ++++++ internal/arch/file/should/match_regex_test.go | 50 ++++++++++ internal/arch/file/should/not_end_with.go | 30 ++++++ .../arch/file/should/not_end_with_test.go | 44 +++++++++ internal/arch/file/should/not_exist.go | 31 ++++++ internal/arch/file/should/not_exist_test.go | 40 ++++++++ internal/arch/file/should/not_match_regex.go | 28 ++++++ .../arch/file/should/not_match_regex_test.go | 52 ++++++++++ internal/arch/file/should/should.go | 22 +++++ internal/arch/file/should/start_with.go | 29 ++++++ internal/arch/file/should/start_with_test.go | 44 +++++++++ internal/arch/{ => file}/test/config/base.yml | 0 internal/arch/file/test/project/Dockerfile | 6 ++ internal/arch/file/test/project/Makefile | 4 + internal/arch/file/that/are_in_folder.go | 67 +++++++++++++ internal/arch/file/that/are_in_folder_test.go | 3 + internal/arch/file/that/end_with.go | 30 ++++++ internal/arch/file/that/end_with_test.go | 3 + internal/arch/rule.go | 98 ------------------- internal/arch/rule/builder.go | 38 +++++++ internal/arch/rule_test.go | 33 ------- internal/arch/should/files.go | 37 ------- internal/arch/slice/main.go | 6 -- internal/arch/test/project/Dockerfile | 0 internal/arch/test/project/Makefile | 0 internal/arch/that/files.go | 93 ------------------ 38 files changed, 928 insertions(+), 291 deletions(-) create mode 100644 go.sum delete mode 100644 internal/arch/expression.go create mode 100644 internal/arch/file/except/except.go create mode 100644 internal/arch/file/except/this.go create mode 100644 internal/arch/file/except/this_test.go create mode 100644 internal/arch/file/rule.go create mode 100644 internal/arch/file/rule_test.go create mode 100644 internal/arch/file/should/end_with.go create mode 100644 internal/arch/file/should/end_with_test.go create mode 100644 internal/arch/file/should/exist.go create mode 100644 internal/arch/file/should/exist_test.go create mode 100644 internal/arch/file/should/match_regex.go create mode 100644 internal/arch/file/should/match_regex_test.go create mode 100644 internal/arch/file/should/not_end_with.go create mode 100644 internal/arch/file/should/not_end_with_test.go create mode 100644 internal/arch/file/should/not_exist.go create mode 100644 internal/arch/file/should/not_exist_test.go create mode 100644 internal/arch/file/should/not_match_regex.go create mode 100644 internal/arch/file/should/not_match_regex_test.go create mode 100644 internal/arch/file/should/should.go create mode 100644 internal/arch/file/should/start_with.go create mode 100644 internal/arch/file/should/start_with_test.go rename internal/arch/{ => file}/test/config/base.yml (100%) create mode 100644 internal/arch/file/test/project/Dockerfile create mode 100644 internal/arch/file/test/project/Makefile create mode 100644 internal/arch/file/that/are_in_folder.go create mode 100644 internal/arch/file/that/are_in_folder_test.go create mode 100644 internal/arch/file/that/end_with.go create mode 100644 internal/arch/file/that/end_with_test.go delete mode 100644 internal/arch/rule.go create mode 100644 internal/arch/rule/builder.go delete mode 100644 internal/arch/rule_test.go delete mode 100644 internal/arch/should/files.go delete mode 100644 internal/arch/slice/main.go delete mode 100644 internal/arch/test/project/Dockerfile delete mode 100644 internal/arch/test/project/Makefile delete mode 100644 internal/arch/that/files.go diff --git a/go.mod b/go.mod index 5db2f6c..efc5bb4 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module goarkitect -go 1.17 +go 1.18 + +require github.com/google/go-cmp v0.5.8 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e9b099c --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= diff --git a/internal/arch/expression.go b/internal/arch/expression.go deleted file mode 100644 index a95aee5..0000000 --- a/internal/arch/expression.go +++ /dev/null @@ -1,23 +0,0 @@ -package arch - -type That interface { - Evaluate(rule *RuleSubjectBuilder) -} - -type Should interface { - Evaluate(rule *RuleSubjectBuilder) []Violation -} - -func NewViolation(message string) Violation { - return Violation{ - message: message, - } -} - -type Violation struct { - message string -} - -func (v Violation) String() string { - return v.message -} diff --git a/internal/arch/file/except/except.go b/internal/arch/file/except/except.go new file mode 100644 index 0000000..301bb4a --- /dev/null +++ b/internal/arch/file/except/except.go @@ -0,0 +1,26 @@ +package except + +import ( + "goarkitect/internal/arch/file" + "goarkitect/internal/arch/rule" + "path/filepath" +) + +type Expression struct { + value string +} + +func (e Expression) Evaluate(rb rule.Builder) { + frb := rb.(*file.RuleBuilder) + + nf := make([]string, 0) + for _, f := range frb.GetFiles() { + if filepath.Base(f) != e.value { + nf = append(nf, f) + } + } + + frb.SetFiles(nf) + + return +} diff --git a/internal/arch/file/except/this.go b/internal/arch/file/except/this.go new file mode 100644 index 0000000..7c12360 --- /dev/null +++ b/internal/arch/file/except/this.go @@ -0,0 +1,7 @@ +package except + +func This(value string) *Expression { + return &Expression{ + value: value, + } +} diff --git a/internal/arch/file/except/this_test.go b/internal/arch/file/except/this_test.go new file mode 100644 index 0000000..3db2cac --- /dev/null +++ b/internal/arch/file/except/this_test.go @@ -0,0 +1,3 @@ +package except_test + +// TODO: implement this diff --git a/internal/arch/file/rule.go b/internal/arch/file/rule.go new file mode 100644 index 0000000..1158604 --- /dev/null +++ b/internal/arch/file/rule.go @@ -0,0 +1,96 @@ +package file + +import ( + "goarkitect/internal/arch/rule" +) + +func All() *RuleBuilder { + return NewRuleBuilder().AllFiles() +} + +func One(filename string) *RuleBuilder { + return NewRuleBuilder().File(filename) +} + +func NewRuleBuilder() *RuleBuilder { + return &RuleBuilder{} +} + +type RuleBuilder struct { + thats []rule.That + excepts []rule.Except + shoulds []rule.Should + because rule.Because + Violations []rule.Violation + + files []string +} + +func (rb *RuleBuilder) That(t rule.That) rule.Builder { + rb.thats = []rule.That{t} + + return rb +} + +func (rb *RuleBuilder) AndThat(t rule.That) rule.Builder { + rb.thats = append(rb.thats, t) + + return rb +} + +func (rb *RuleBuilder) Except(s ...rule.Except) rule.Builder { + rb.excepts = s + + return rb +} + +func (rb *RuleBuilder) Should(e rule.Should) rule.Builder { + rb.shoulds = []rule.Should{e} + + return rb +} + +func (rb *RuleBuilder) AndShould(e rule.Should) rule.Builder { + rb.shoulds = append(rb.shoulds, e) + + return rb +} + +func (rb *RuleBuilder) Because(b rule.Because) []rule.Violation { + rb.because = b + + for _, that := range rb.thats { + that.Evaluate(rb) + } + + for _, except := range rb.excepts { + except.Evaluate(rb) + } + + for _, should := range rb.shoulds { + rb.Violations = should.Evaluate(rb) + if len(rb.Violations) > 0 { + return rb.Violations + } + } + + return nil +} + +func (rb *RuleBuilder) GetFiles() []string { + return rb.files +} + +func (rb *RuleBuilder) SetFiles(files []string) { + rb.files = files +} + +func (rb *RuleBuilder) AllFiles() *RuleBuilder { + return rb +} + +func (rb *RuleBuilder) File(filename string) *RuleBuilder { + rb.files = []string{filename} + + return rb +} diff --git a/internal/arch/file/rule_test.go b/internal/arch/file/rule_test.go new file mode 100644 index 0000000..4dab6f1 --- /dev/null +++ b/internal/arch/file/rule_test.go @@ -0,0 +1,93 @@ +package file_test + +import ( + "goarkitect/internal/arch/file" + fe "goarkitect/internal/arch/file/except" + fs "goarkitect/internal/arch/file/should" + ft "goarkitect/internal/arch/file/that" + "os" + "path/filepath" + "testing" +) + +func Test_It_Checks_A_File_Exists(t *testing.T) { + filename := "./test/project/Makefile" + + vs := file.One(filename). + Should(fs.Exist()). + Because("it is needed to encapsulate common project operations") + + if vs != nil { + for _, v := range vs { + t.Errorf("%v", v) + } + } +} + +func Test_It_Checks_Multiple_Files_Exists_When_They_Do(t *testing.T) { + basePath, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + vs := file.All(). + That(ft.AreInFolder(filepath.Join(basePath, "test/project"), false)). + Should(fs.Exist()). + AndShould(fs.EndWith("file")). + Because("they are needed for developing and deploying the project") + + if vs != nil { + for _, v := range vs { + t.Errorf("%v", v) + } + } +} + +func Test_It_Checks_Multiple_Files_Exists_When_They_Dont(t *testing.T) { + basePath, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + vs := file.All(). + That(ft.AreInFolder(filepath.Join(basePath, "test/project"), false)). + Should(fs.Exist()). + AndShould(fs.EndWith(".yaml")). + Because("they are needed for developing and deploying the project") + + if vs == nil { + t.Errorf("Expected violations, got nil") + } + + if len(vs) != 2 { + t.Errorf("Expected 2 violations, got %d", len(vs)) + } + + if vs[0].String() != "file's name 'Dockerfile' does not end with '.yaml'" { + t.Errorf("Expected violation for Dockerfile, got: '%s'", vs[0].String()) + } + + if vs[1].String() != "file's name 'Makefile' does not end with '.yaml'" { + t.Errorf("Expected violation for Makefile, got: '%s'", vs[1].String()) + } +} + +func Test_It_Checks_Multiple_Files_Exists_When_They_Do_With_Exception(t *testing.T) { + basePath, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + vs := file.All(). + That(ft.AreInFolder(filepath.Join(basePath, "test/project"), false)). + Should(fs.Exist()). + AndShould(fs.StartWith("Docker")). + Except(fe.This("Makefile"), fe.This("Something")). + Because("they are needed for developing and deploying the project") + + if vs != nil { + for _, v := range vs { + t.Errorf("%v", v) + } + } +} diff --git a/internal/arch/file/should/end_with.go b/internal/arch/file/should/end_with.go new file mode 100644 index 0000000..91d7fa0 --- /dev/null +++ b/internal/arch/file/should/end_with.go @@ -0,0 +1,30 @@ +package should + +import ( + "fmt" + "goarkitect/internal/arch/rule" + "path/filepath" +) + +func EndWith(suffix string) *Expression { + ls := len(suffix) + + return &Expression{ + checkViolation: func(filePath string) bool { + fileName := filepath.Base(filePath) + + lf := len(fileName) + + return ls <= lf && fileName[lf-ls:] != suffix + }, + getViolation: func(filePath string) rule.Violation { + return rule.NewViolation( + fmt.Sprintf( + "file's name '%s' does not end with '%s'", + filepath.Base(filePath), + suffix, + ), + ) + }, + } +} diff --git a/internal/arch/file/should/end_with_test.go b/internal/arch/file/should/end_with_test.go new file mode 100644 index 0000000..50866f6 --- /dev/null +++ b/internal/arch/file/should/end_with_test.go @@ -0,0 +1,44 @@ +package should_test + +import ( + "goarkitect/internal/arch/file" + "goarkitect/internal/arch/file/should" + "goarkitect/internal/arch/rule" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func Test_EndWith(t *testing.T) { + testCases := []struct { + desc string + ruleBuilder *file.RuleBuilder + suffix string + want []rule.Violation + }{ + { + desc: "foobar ends with bar", + ruleBuilder: file.One("foobar"), + suffix: "bar", + want: nil, + }, + { + desc: "foobar does not end with baz", + ruleBuilder: file.One("foobar"), + suffix: "baz", + want: []rule.Violation{ + rule.NewViolation("file's name 'foobar' does not end with 'baz'"), + }, + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + ew := should.EndWith(tC.suffix) + got := ew.Evaluate(tC.ruleBuilder) + if !cmp.Equal(got, tC.want, cmp.AllowUnexported(rule.Violation{}), cmpopts.EquateEmpty()) { + t.Errorf("want = %+v, got = %+v", tC.want, got) + } + }) + } +} diff --git a/internal/arch/file/should/exist.go b/internal/arch/file/should/exist.go new file mode 100644 index 0000000..524b519 --- /dev/null +++ b/internal/arch/file/should/exist.go @@ -0,0 +1,32 @@ +package should + +import ( + "fmt" + "goarkitect/internal/arch/rule" + "os" + "path/filepath" +) + +func Exist() *Expression { + return &Expression{ + checkViolation: func(filePath string) bool { + if _, err := os.Stat(filePath); err != nil { + if os.IsNotExist(err) { + return true + } + + panic(err) + } + + return false + }, + getViolation: func(filePath string) rule.Violation { + return rule.NewViolation( + fmt.Sprintf( + "file '%s' does not exist", + filepath.Base(filePath), + ), + ) + }, + } +} diff --git a/internal/arch/file/should/exist_test.go b/internal/arch/file/should/exist_test.go new file mode 100644 index 0000000..9dd1f09 --- /dev/null +++ b/internal/arch/file/should/exist_test.go @@ -0,0 +1,42 @@ +package should_test + +import ( + "goarkitect/internal/arch/file" + "goarkitect/internal/arch/file/should" + "goarkitect/internal/arch/rule" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func Test_Exist(t *testing.T) { + testCases := []struct { + desc string + ruleBuilder *file.RuleBuilder + suffix string + want []rule.Violation + }{ + { + desc: "exist.go exists", + ruleBuilder: file.One("exist.go"), + want: nil, + }, + { + desc: "abc.xyz does not exist", + ruleBuilder: file.One("abc.xyz"), + want: []rule.Violation{ + rule.NewViolation("file 'abc.xyz' does not exist"), + }, + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + e := should.Exist() + got := e.Evaluate(tC.ruleBuilder) + if !cmp.Equal(got, tC.want, cmp.AllowUnexported(rule.Violation{}), cmpopts.EquateEmpty()) { + t.Errorf("want = %+v, got = %+v", tC.want, got) + } + }) + } +} diff --git a/internal/arch/file/should/match_regex.go b/internal/arch/file/should/match_regex.go new file mode 100644 index 0000000..6dba2ee --- /dev/null +++ b/internal/arch/file/should/match_regex.go @@ -0,0 +1,29 @@ +package should + +import ( + "fmt" + "goarkitect/internal/arch/rule" + "path/filepath" + "regexp" +) + +func MatchRegex(res string) *Expression { + rx := regexp.MustCompile(res) + + return &Expression{ + checkViolation: func(filePath string) bool { + return !rx.MatchString( + filepath.Base(filePath), + ) + }, + getViolation: func(filePath string) rule.Violation { + return rule.NewViolation( + fmt.Sprintf( + "file's name '%s' does not match regex '%s'", + filepath.Base(filePath), + res, + ), + ) + }, + } +} diff --git a/internal/arch/file/should/match_regex_test.go b/internal/arch/file/should/match_regex_test.go new file mode 100644 index 0000000..b73e818 --- /dev/null +++ b/internal/arch/file/should/match_regex_test.go @@ -0,0 +1,50 @@ +package should_test + +import ( + "goarkitect/internal/arch/file" + "goarkitect/internal/arch/file/should" + "goarkitect/internal/arch/rule" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func Test_MatchRegex(t *testing.T) { + testCases := []struct { + desc string + ruleBuilder *file.RuleBuilder + regexp string + want []rule.Violation + }{ + { + desc: "foobar matches '[a-z]+'", + ruleBuilder: file.One("foobar"), + regexp: "[a-z]+", + want: nil, + }, + { + desc: "foobar matches 'foobar'", + ruleBuilder: file.One("foobar"), + regexp: "foobar", + want: nil, + }, + { + desc: "foobar does not match '[0-9]+'", + ruleBuilder: file.One("foobar"), + regexp: "[0-9]+", + want: []rule.Violation{ + rule.NewViolation("file's name 'foobar' does not match regex '[0-9]+'"), + }, + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + ew := should.MatchRegex(tC.regexp) + got := ew.Evaluate(tC.ruleBuilder) + if !cmp.Equal(got, tC.want, cmp.AllowUnexported(rule.Violation{}), cmpopts.EquateEmpty()) { + t.Errorf("want = %+v, got = %+v", tC.want, got) + } + }) + } +} diff --git a/internal/arch/file/should/not_end_with.go b/internal/arch/file/should/not_end_with.go new file mode 100644 index 0000000..2c91c53 --- /dev/null +++ b/internal/arch/file/should/not_end_with.go @@ -0,0 +1,30 @@ +package should + +import ( + "fmt" + "goarkitect/internal/arch/rule" + "path/filepath" +) + +func NotEndWith(suffix string) *Expression { + ls := len(suffix) + + return &Expression{ + checkViolation: func(filePath string) bool { + fileName := filepath.Base(filePath) + + lf := len(fileName) + + return ls > lf || fileName[lf-ls:] == suffix + }, + getViolation: func(filePath string) rule.Violation { + return rule.NewViolation( + fmt.Sprintf( + "file's name '%s' does end with '%s'", + filepath.Base(filePath), + suffix, + ), + ) + }, + } +} diff --git a/internal/arch/file/should/not_end_with_test.go b/internal/arch/file/should/not_end_with_test.go new file mode 100644 index 0000000..7575812 --- /dev/null +++ b/internal/arch/file/should/not_end_with_test.go @@ -0,0 +1,44 @@ +package should_test + +import ( + "goarkitect/internal/arch/file" + "goarkitect/internal/arch/file/should" + "goarkitect/internal/arch/rule" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func Test_NotEndWith(t *testing.T) { + testCases := []struct { + desc string + ruleBuilder *file.RuleBuilder + suffix string + want []rule.Violation + }{ + { + desc: "foobar does not end with baz", + ruleBuilder: file.One("foobar"), + suffix: "baz", + want: nil, + }, + { + desc: "foobar ends with bar", + ruleBuilder: file.One("foobar"), + suffix: "bar", + want: []rule.Violation{ + rule.NewViolation("file's name 'foobar' does end with 'bar'"), + }, + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + ew := should.NotEndWith(tC.suffix) + got := ew.Evaluate(tC.ruleBuilder) + if !cmp.Equal(got, tC.want, cmp.AllowUnexported(rule.Violation{}), cmpopts.EquateEmpty()) { + t.Errorf("want = %+v, got = %+v", tC.want, got) + } + }) + } +} diff --git a/internal/arch/file/should/not_exist.go b/internal/arch/file/should/not_exist.go new file mode 100644 index 0000000..dd314b8 --- /dev/null +++ b/internal/arch/file/should/not_exist.go @@ -0,0 +1,31 @@ +package should + +import ( + "fmt" + "goarkitect/internal/arch/rule" + "os" + "path/filepath" +) + +func NotExist() *Expression { + return &Expression{ + checkViolation: func(filePath string) bool { + if _, err := os.Stat(filePath); err != nil { + if os.IsNotExist(err) { + return false + } + panic(err) + } + + return true + }, + getViolation: func(filePath string) rule.Violation { + return rule.NewViolation( + fmt.Sprintf( + "file '%s' does exist", + filepath.Base(filePath), + ), + ) + }, + } +} diff --git a/internal/arch/file/should/not_exist_test.go b/internal/arch/file/should/not_exist_test.go new file mode 100644 index 0000000..7f0a584 --- /dev/null +++ b/internal/arch/file/should/not_exist_test.go @@ -0,0 +1,40 @@ +package should_test + +import ( + "goarkitect/internal/arch/file" + "goarkitect/internal/arch/file/should" + "goarkitect/internal/arch/rule" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func Test_NotExist(t *testing.T) { + testCases := []struct { + desc string + ruleBuilder *file.RuleBuilder + want []rule.Violation + }{ + { + desc: "exist.go exists", + ruleBuilder: file.One("exist.go"), + want: []rule.Violation{ + rule.NewViolation("file 'exist.go' does exist"), + }, + }, + { + desc: "abc.xyz does not exist", + ruleBuilder: file.One("abc.xyz"), + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + e := should.NotExist() + got := e.Evaluate(tC.ruleBuilder) + if !cmp.Equal(got, tC.want, cmp.AllowUnexported(rule.Violation{}), cmpopts.EquateEmpty()) { + t.Errorf("want = %+v, got = %+v", tC.want, got) + } + }) + } +} diff --git a/internal/arch/file/should/not_match_regex.go b/internal/arch/file/should/not_match_regex.go new file mode 100644 index 0000000..0e54fef --- /dev/null +++ b/internal/arch/file/should/not_match_regex.go @@ -0,0 +1,28 @@ +package should + +import ( + "fmt" + "goarkitect/internal/arch/rule" + "path/filepath" + "regexp" +) + +func NotMatchRegex(res string) *Expression { + rx := regexp.MustCompile(res) + + return &Expression{ + checkViolation: func(filePath string) bool { + fileName := filepath.Base(filePath) + return rx.MatchString(fileName) + }, + getViolation: func(filePath string) rule.Violation { + return rule.NewViolation( + fmt.Sprintf( + "file's name '%s' does not match regex '%s'", + filepath.Base(filePath), + res, + ), + ) + }, + } +} diff --git a/internal/arch/file/should/not_match_regex_test.go b/internal/arch/file/should/not_match_regex_test.go new file mode 100644 index 0000000..96ad47a --- /dev/null +++ b/internal/arch/file/should/not_match_regex_test.go @@ -0,0 +1,52 @@ +package should_test + +import ( + "goarkitect/internal/arch/file" + "goarkitect/internal/arch/file/should" + "goarkitect/internal/arch/rule" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func Test_NotMatchRegex(t *testing.T) { + testCases := []struct { + desc string + ruleBuilder *file.RuleBuilder + regexp string + want []rule.Violation + }{ + { + desc: "foobar matches '[a-z]+'", + ruleBuilder: file.One("foobar"), + regexp: "[a-z]+", + want: []rule.Violation{ + rule.NewViolation("file's name 'foobar' does not match regex '[a-z]+'"), + }, + }, + { + desc: "foobar matches 'foobar'", + ruleBuilder: file.One("foobar"), + regexp: "foobar", + want: []rule.Violation{ + rule.NewViolation("file's name 'foobar' does not match regex 'foobar'"), + }, + }, + { + desc: "foobar does not match '[0-9]+'", + ruleBuilder: file.One("foobar"), + regexp: "[0-9]+", + want: nil, + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + ew := should.NotMatchRegex(tC.regexp) + got := ew.Evaluate(tC.ruleBuilder) + if !cmp.Equal(got, tC.want, cmp.AllowUnexported(rule.Violation{}), cmpopts.EquateEmpty()) { + t.Errorf("want = %+v, got = %+v", tC.want, got) + } + }) + } +} diff --git a/internal/arch/file/should/should.go b/internal/arch/file/should/should.go new file mode 100644 index 0000000..4e5b829 --- /dev/null +++ b/internal/arch/file/should/should.go @@ -0,0 +1,22 @@ +package should + +import ( + "goarkitect/internal/arch/file" + "goarkitect/internal/arch/rule" +) + +type Expression struct { + checkViolation func(filePath string) bool + getViolation func(filePath string) rule.Violation +} + +func (e Expression) Evaluate(rb rule.Builder) []rule.Violation { + violations := make([]rule.Violation, 0) + for _, filePath := range rb.(*file.RuleBuilder).GetFiles() { + if e.checkViolation(filePath) { + violations = append(violations, e.getViolation(filePath)) + } + } + + return violations +} diff --git a/internal/arch/file/should/start_with.go b/internal/arch/file/should/start_with.go new file mode 100644 index 0000000..8b624c6 --- /dev/null +++ b/internal/arch/file/should/start_with.go @@ -0,0 +1,29 @@ +package should + +import ( + "fmt" + "goarkitect/internal/arch/rule" + "path/filepath" +) + +func StartWith(prefix string) *Expression { + le := len(prefix) + + return &Expression{ + checkViolation: func(filePath string) bool { + fileName := filepath.Base(filePath) + lf := len(fileName) + + return le <= lf && fileName[:le] != prefix + }, + getViolation: func(filePath string) rule.Violation { + return rule.NewViolation( + fmt.Sprintf( + "file's name '%s' does not start with '%s'", + filepath.Base(filePath), + prefix, + ), + ) + }, + } +} diff --git a/internal/arch/file/should/start_with_test.go b/internal/arch/file/should/start_with_test.go new file mode 100644 index 0000000..066f18e --- /dev/null +++ b/internal/arch/file/should/start_with_test.go @@ -0,0 +1,44 @@ +package should_test + +import ( + "goarkitect/internal/arch/file" + "goarkitect/internal/arch/file/should" + "goarkitect/internal/arch/rule" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func Test_StartWith(t *testing.T) { + testCases := []struct { + desc string + ruleBuilder *file.RuleBuilder + prefix string + want []rule.Violation + }{ + { + desc: "foobar starts with foo", + ruleBuilder: file.One("foobar"), + prefix: "foo", + want: nil, + }, + { + desc: "foobar does not start with baz", + ruleBuilder: file.One("foobar"), + prefix: "baz", + want: []rule.Violation{ + rule.NewViolation("file's name 'foobar' does not start with 'baz'"), + }, + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + ew := should.StartWith(tC.prefix) + got := ew.Evaluate(tC.ruleBuilder) + if !cmp.Equal(got, tC.want, cmp.AllowUnexported(rule.Violation{}), cmpopts.EquateEmpty()) { + t.Errorf("want = %+v, got = %+v", tC.want, got) + } + }) + } +} diff --git a/internal/arch/test/config/base.yml b/internal/arch/file/test/config/base.yml similarity index 100% rename from internal/arch/test/config/base.yml rename to internal/arch/file/test/config/base.yml diff --git a/internal/arch/file/test/project/Dockerfile b/internal/arch/file/test/project/Dockerfile new file mode 100644 index 0000000..ca77ed3 --- /dev/null +++ b/internal/arch/file/test/project/Dockerfile @@ -0,0 +1,6 @@ +FROM alpine AS test + +RUN apk update +RUN apk add bash curl + +ENTRYPOINT /bin/bash diff --git a/internal/arch/file/test/project/Makefile b/internal/arch/file/test/project/Makefile new file mode 100644 index 0000000..9824069 --- /dev/null +++ b/internal/arch/file/test/project/Makefile @@ -0,0 +1,4 @@ +.PHONY: cake + +cake: + echo "it is a lie." diff --git a/internal/arch/file/that/are_in_folder.go b/internal/arch/file/that/are_in_folder.go new file mode 100644 index 0000000..7d9d53d --- /dev/null +++ b/internal/arch/file/that/are_in_folder.go @@ -0,0 +1,67 @@ +package that + +import ( + "goarkitect/internal/arch/file" + "goarkitect/internal/arch/rule" + "io/ioutil" + "log" + "os" + "path/filepath" +) + +func AreInFolder(folder string, recursive bool) *AreInFolderExpression { + return &AreInFolderExpression{ + folder: folder, + recursive: recursive, + } +} + +type AreInFolderExpression struct { + folder string + recursive bool +} + +func (e *AreInFolderExpression) Evaluate(rb rule.Builder) { + frb := rb.(*file.RuleBuilder) + + if e.recursive { + frb.SetFiles(e.getFilesRecursive(e.folder)) + } else { + frb.SetFiles(e.getFiles(e.folder)) + } +} + +func (e *AreInFolderExpression) getFilesRecursive(folder string) []string { + var filenames []string + if err := filepath.Walk(folder, e.visit(&filenames)); err != nil { + log.Println(err) + return nil + } + + return filenames +} + +func (e *AreInFolderExpression) visit(files *[]string) filepath.WalkFunc { + return func(path string, _ os.FileInfo, err error) error { + if err != nil { + return err + } + *files = append(*files, path) + + return nil + } +} + +func (e *AreInFolderExpression) getFiles(folder string) []string { + files, err := ioutil.ReadDir(folder) + if err != nil { + panic(err) + } + + filePaths := make([]string, len(files)) + for i, file := range files { + filePaths[i] = filepath.Join(folder, file.Name()) + } + + return filePaths +} diff --git a/internal/arch/file/that/are_in_folder_test.go b/internal/arch/file/that/are_in_folder_test.go new file mode 100644 index 0000000..3a5bb5c --- /dev/null +++ b/internal/arch/file/that/are_in_folder_test.go @@ -0,0 +1,3 @@ +package that_test + +// TODO: implement diff --git a/internal/arch/file/that/end_with.go b/internal/arch/file/that/end_with.go new file mode 100644 index 0000000..5519e20 --- /dev/null +++ b/internal/arch/file/that/end_with.go @@ -0,0 +1,30 @@ +package that + +import ( + "goarkitect/internal/arch/file" + "goarkitect/internal/arch/rule" + "strings" +) + +func EndWith(s string) *EndWithExpression { + return &EndWithExpression{ + suffix: s, + } +} + +type EndWithExpression struct { + suffix string +} + +func (e EndWithExpression) Evaluate(rb rule.Builder) { + frb := rb.(*file.RuleBuilder) + + files := make([]string, 0) + for _, f := range frb.GetFiles() { + if strings.HasSuffix(f, e.suffix) { + files = append(files, f) + } + } + + frb.SetFiles(files) +} diff --git a/internal/arch/file/that/end_with_test.go b/internal/arch/file/that/end_with_test.go new file mode 100644 index 0000000..3a5bb5c --- /dev/null +++ b/internal/arch/file/that/end_with_test.go @@ -0,0 +1,3 @@ +package that_test + +// TODO: implement diff --git a/internal/arch/rule.go b/internal/arch/rule.go deleted file mode 100644 index 4575a25..0000000 --- a/internal/arch/rule.go +++ /dev/null @@ -1,98 +0,0 @@ -package arch - -import "goarkitect/internal/arch/slice" - -type RuleKind int - -const ( - FilesRuleKind RuleKind = iota -) - -func NewRule() *RuleSubjectBuilder { - return &RuleSubjectBuilder{} -} - -type RuleSubjectBuilder struct { - kind RuleKind - Files []string - thats []That - excepts []string - shoulds []Should - because string -} - -func (rb *RuleSubjectBuilder) init() { - rb.kind = FilesRuleKind - rb.Files = []string{} - rb.thats = []That{} - rb.excepts = []string{} - rb.shoulds = []Should{} - rb.because = "" -} - -func (rb *RuleSubjectBuilder) Kind() RuleKind { - return rb.kind -} - -func (rb *RuleSubjectBuilder) AllFiles() *RuleSubjectBuilder { - rb.init() - - rb.kind = FilesRuleKind - - return rb -} - -func (rb *RuleSubjectBuilder) That(t That) *RuleSubjectBuilder { - rb.thats = []That{t} - - return rb -} - -func (rb *RuleSubjectBuilder) AndThat(t That) *RuleSubjectBuilder { - rb.thats = append(rb.thats, t) - - return rb -} - -func (rb *RuleSubjectBuilder) Except(s ...string) *RuleSubjectBuilder { - rb.excepts = s - - return rb -} - -func (rb *RuleSubjectBuilder) Should(e Should) *RuleSubjectBuilder { - rb.shoulds = []Should{e} - - return rb -} - -func (rb *RuleSubjectBuilder) AndShould(e Should) *RuleSubjectBuilder { - rb.shoulds = append(rb.shoulds, e) - - return rb -} - -func (rb *RuleSubjectBuilder) Because(b string) bool { - rb.because = b - - for _, that := range rb.thats { - that.Evaluate(rb) - } - - for _, except := range rb.excepts { - for j, file := range rb.Files { - if file == except { - slice.Remove(rb.Files, j) - } - } - } - - for _, should := range rb.shoulds { - violations := should.Evaluate(rb) - if len(violations) > 0 { - return false - } - } - - return true -} diff --git a/internal/arch/rule/builder.go b/internal/arch/rule/builder.go new file mode 100644 index 0000000..2501a89 --- /dev/null +++ b/internal/arch/rule/builder.go @@ -0,0 +1,38 @@ +package rule + +type Builder interface { + That(t That) Builder + AndThat(t That) Builder + Except(s ...Except) Builder + Should(e Should) Builder + AndShould(e Should) Builder + Because(b Because) []Violation +} + +type That interface { + Evaluate(rule Builder) +} + +type Except interface { + Evaluate(rule Builder) +} + +type Should interface { + Evaluate(rule Builder) []Violation +} + +type Because string + +func NewViolation(message string) Violation { + return Violation{ + message: message, + } +} + +type Violation struct { + message string +} + +func (v Violation) String() string { + return v.message +} diff --git a/internal/arch/rule_test.go b/internal/arch/rule_test.go deleted file mode 100644 index 5adf8d4..0000000 --- a/internal/arch/rule_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package arch_test - -import ( - "goarkitect/internal/arch" - "goarkitect/internal/arch/should" - "goarkitect/internal/arch/that" - "testing" -) - -//func TestRuleFileExist(t *testing.T) { -// filename := "test/project/Makefile" -// -// r := arch.NewFileRule(filename).Should(arch.NewExists()) -// -// if r.Validate() != true { -// t.Errorf("expected file %s to exist, it did not", filename) -// } -//} - -func TestRuleFilesExist(t *testing.T) { - filenames := []string{"test/project/Makefile", "test/project/Dockerfile"} - - r := arch.NewRule().AllFiles(). - That(that.AreInFolder("test/project", false)). - AndThat(that.EndsWith("file")). - Except("Makefile"). - Should(should.Exist()). - Because("some reason") - - if r != true { - t.Errorf("expected Files %s to exist, they did not", filenames) - } -} diff --git a/internal/arch/should/files.go b/internal/arch/should/files.go deleted file mode 100644 index 355deae..0000000 --- a/internal/arch/should/files.go +++ /dev/null @@ -1,37 +0,0 @@ -package should - -import ( - "goarkitect/internal/arch" - "fmt" - "os" -) - -func Exist() *ExistExpression { - return &ExistExpression{} -} - -type ExistExpression struct { -} - -func (e ExistExpression) Evaluate(rule *arch.RuleSubjectBuilder) []arch.Violation { - if rule.Kind() != arch.FilesRuleKind { - panic("ExistExpression can only be used with files rules.") - } - - violations := make([]arch.Violation, len(rule.Files)) - for _, file := range rule.Files { - if _, err := os.Stat(file); os.IsNotExist(err) { - violations = append(violations, - arch.NewViolation( - fmt.Sprintf("File %s does not exist.", file), - ), - ) - } - } - - if len(violations) > 0 { - return violations - } - - return nil -} diff --git a/internal/arch/slice/main.go b/internal/arch/slice/main.go deleted file mode 100644 index fdda470..0000000 --- a/internal/arch/slice/main.go +++ /dev/null @@ -1,6 +0,0 @@ -package slice - -func Remove(slice []string, i int) []string { - copy(slice[i:], slice[i+1:]) - return slice[:len(slice)-1] -} diff --git a/internal/arch/test/project/Dockerfile b/internal/arch/test/project/Dockerfile deleted file mode 100644 index e69de29..0000000 diff --git a/internal/arch/test/project/Makefile b/internal/arch/test/project/Makefile deleted file mode 100644 index e69de29..0000000 diff --git a/internal/arch/that/files.go b/internal/arch/that/files.go deleted file mode 100644 index 486e3b5..0000000 --- a/internal/arch/that/files.go +++ /dev/null @@ -1,93 +0,0 @@ -package that - -import ( - "goarkitect/internal/arch" - "io/ioutil" - "log" - "os" - "path/filepath" - "strings" -) - -func AreInFolder(folder string, recursive bool) *AreInFolderExpression { - return &AreInFolderExpression{ - folder: folder, - recursive: recursive, - } -} - -type AreInFolderExpression struct { - folder string - recursive bool -} - -func (e *AreInFolderExpression) Evaluate(rule *arch.RuleSubjectBuilder) { - if rule.Kind() != arch.FilesRuleKind { - panic("AreInFolderExpression can only be used with files rules.") - } - - if e.recursive { - rule.Files = e.getFilesRecursive(e.folder) - } else { - rule.Files = e.getFiles(e.folder) - } -} - -func (e *AreInFolderExpression) getFilesRecursive(folder string) []string { - var filenames []string - err := filepath.Walk(folder, e.visit(&filenames)) - if err != nil { - log.Println(err) - return nil - } - return filenames -} - -func (e *AreInFolderExpression) visit(files *[]string) filepath.WalkFunc { - return func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - *files = append(*files, path) - return nil - } -} - -func (e *AreInFolderExpression) getFiles(folder string) []string { - files, err := ioutil.ReadDir(folder) - if err != nil { - log.Println(err) - return nil - } - - filenames := make([]string, len(files)) - for i, file := range files { - filenames[i] = file.Name() - } - - return filenames -} - -func EndsWith(s string) *EndsWithExpression { - return &EndsWithExpression{ - suffix: s, - } -} - -type EndsWithExpression struct { - suffix string -} - -func (e EndsWithExpression) Evaluate(rule *arch.RuleSubjectBuilder) { - if rule.Kind() != arch.FilesRuleKind { - panic("AreInFolderExpression can only be used with files rules.") - } - - files := make([]string, 0) - for _, f := range rule.Files { - if strings.HasSuffix(f, e.suffix) { - files = append(files, f) - } - } - rule.Files = files -}