diff --git a/README.md b/README.md index 68a02fc..e1619b6 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ if a file: - [x] content matches value - [x] content matches regex - [ ] content matches template +- [ ] content contains a value - [ ] is gitignored - [ ] is gitcrypted diff --git a/internal/arch/file/should/contain_value.go b/internal/arch/file/should/contain_value.go new file mode 100644 index 0000000..73f1a4d --- /dev/null +++ b/internal/arch/file/should/contain_value.go @@ -0,0 +1,59 @@ +package should + +import ( + "bytes" + "fmt" + "goarkitect/internal/arch/rule" + "os" + "path/filepath" +) + +func ContainValue(value []byte, opts ...Option) *containValueExpression { + expr := &containValueExpression{ + value: value, + } + + for _, opt := range opts { + opt.apply(&expr.options) + } + + return expr +} + +type containValueExpression struct { + baseExpression + + value []byte +} + +func (e containValueExpression) Evaluate(rb rule.Builder) []rule.Violation { + return e.evaluate(rb, e.doEvaluate, e.getViolation) +} + +func (e containValueExpression) doEvaluate(rb rule.Builder, filePath string) bool { + data, err := os.ReadFile(filePath) + if err != nil { + rb.AddError(err) + + return true + } + + if e.options.ignoreCase { + data = bytes.ToLower(data) + e.value = bytes.ToLower(e.value) + } + + return !bytes.Contains(data, e.value) +} + +func (e containValueExpression) getViolation(filePath string) rule.Violation { + format := "file '%s' does not contain the value '%s'" + + if e.options.negated { + format = "file '%s' does contain the value '%s'" + } + + return rule.NewViolation( + fmt.Sprintf(format, filepath.Base(filePath), e.value), + ) +} diff --git a/internal/arch/file/should/contain_value_test.go b/internal/arch/file/should/contain_value_test.go new file mode 100644 index 0000000..fab9eaf --- /dev/null +++ b/internal/arch/file/should/contain_value_test.go @@ -0,0 +1,63 @@ +package should_test + +import ( + "goarkitect/internal/arch/file" + "goarkitect/internal/arch/file/should" + "goarkitect/internal/arch/rule" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func Test_ContainValue(t *testing.T) { + basePath, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + testCases := []struct { + desc string + ruleBuilder *file.RuleBuilder + value string + options []should.Option + want []rule.Violation + }{ + { + desc: "file 'foobar.txt' contains the value 'bar'", + ruleBuilder: file.One(filepath.Join(basePath, "test/foobar.txt")), + value: "bar", + want: nil, + }, + { + desc: "file 'foobar.txt' contains the value 'bar', ignoring case", + ruleBuilder: file.One(filepath.Join(basePath, "test/foobar.txt")), + value: "BAR", + options: []should.Option{ + should.IgnoreCase{}, + }, + want: nil, + }, + { + desc: "file 'foobar.txt' does not contain the value 'something else'", + ruleBuilder: file.One(filepath.Join(basePath, "test/foobar.txt")), + value: "something else", + want: []rule.Violation{ + rule.NewViolation("file 'foobar.txt' does not contain the value 'something else'"), + }, + }, + } + + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + hcm := should.ContainValue([]byte(tC.value), tC.options...) + got := hcm.Evaluate(tC.ruleBuilder) + + if !cmp.Equal(got, tC.want, cmp.AllowUnexported(rule.Violation{}), cmpopts.EquateEmpty()) { + t.Errorf("want = %+v, got = %+v", tC.want, got) + } + }) + } +}