From 0d6067dda53c204bb74ae824e2cbb390685d6a90 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Mon, 12 Nov 2018 16:42:24 +0100 Subject: [PATCH] implement nginx/encoder + decode->encode test --- internal/cnf/parser/nginx/decoder.go | 18 ---- internal/cnf/parser/nginx/decoder_test.go | 6 +- internal/cnf/parser/nginx/encoder.go | 87 ++++++++++++++++++ internal/cnf/parser/nginx/encoder_test.go | 102 ++++++++++++++++++++++ internal/cnf/parser/nginx/errors.go | 17 ++++ internal/cnf/parser/nginx/nginx.go | 7 +- 6 files changed, 214 insertions(+), 23 deletions(-) create mode 100644 internal/cnf/parser/nginx/encoder_test.go diff --git a/internal/cnf/parser/nginx/decoder.go b/internal/cnf/parser/nginx/decoder.go index 9030c74..71d331e 100644 --- a/internal/cnf/parser/nginx/decoder.go +++ b/internal/cnf/parser/nginx/decoder.go @@ -2,29 +2,11 @@ package nginx import ( "bufio" - "fmt" "io" "regexp" "strings" ) -// ErrNullReceiver is raised when a null receiver is provided -var ErrNullReceiver = fmt.Errorf("receiver must not be null") - -// ErrInvalidReceiver is raised when an invalid receiver is provided -var ErrInvalidReceiver = fmt.Errorf("receiver must be compatible with *[]*Line") - -// ErrUnexpectedSectionClose is raised when reading a section close '}' without an -// open section -var ErrUnexpectedSectionClose = fmt.Errorf("unexpected section close") - -// ErrUnclosedSection is raised when there is unclosed sections reaching the end -// of the file -var ErrUnclosedSection = fmt.Errorf("unclosed section") - -// ErrInvalidSyntax is raised when a line cannot be understood -var ErrInvalidSyntax = fmt.Errorf("invalid syntax") - // decoder implements parser.Decoder type decoder struct{ reader io.Reader } diff --git a/internal/cnf/parser/nginx/decoder_test.go b/internal/cnf/parser/nginx/decoder_test.go index c391b68..4c5789a 100644 --- a/internal/cnf/parser/nginx/decoder_test.go +++ b/internal/cnf/parser/nginx/decoder_test.go @@ -54,8 +54,7 @@ func TestEachLineType(t *testing.T) { for i, test := range tests { // 1. create reader - parser := new(nginx) - decoder := parser.NewDecoder(strings.NewReader(test.Raw)) + decoder := NewDecoder(strings.NewReader(test.Raw)) // 2. Decode receiver := new(nginx) @@ -114,8 +113,7 @@ func TestNestedSections(t *testing.T) { for i, test := range tests { // 1. create reader - parser := new(nginx) - decoder := parser.NewDecoder(strings.NewReader(test.Raw)) + decoder := NewDecoder(strings.NewReader(test.Raw)) // 2. Decode receiver := new(nginx) diff --git a/internal/cnf/parser/nginx/encoder.go b/internal/cnf/parser/nginx/encoder.go index 357ba3d..e3c17c0 100644 --- a/internal/cnf/parser/nginx/encoder.go +++ b/internal/cnf/parser/nginx/encoder.go @@ -1,6 +1,7 @@ package nginx import ( + "fmt" "io" ) @@ -22,5 +23,91 @@ func (e *encoder) SetIndent(prefix, indent string) { } func (e *encoder) Encode(v interface{}) error { + // check 'v' + vcast, ok := v.(*nginx) + if !ok { + return ErrInvalidReceiver + } + + // empty config + if len(vcast.Lines) < 1 { + return nil + } + + // actual indentation level + indent := 0 + + // actual lines to write + stack := make([]*Line, 0) + + // push nginx.Lines in reverse order + for l := len(vcast.Lines) - 1; l >= 0; l-- { + stack = append(stack, vcast.Lines[l]) + } + + for len(stack) > 0 { + + // pop stack + line := stack[len(stack)-1] + stack = stack[:len(stack)-1] + + // line representation + repr := make([]byte, 0) + + if line == nil { // section end + indent-- + } + + // 1. add indentation + repr = append(repr, []byte(e.prefix)...) + for i := 0; i < indent; i++ { + repr = append(repr, []byte(e.indent)...) + } + + // 2. section end + if line == nil { + repr = append(repr, []byte("}\n\n")...) + // Note: new line after section for readability + indent-- + + // 3. comments + } else if line.Type == COMMENT { + + repr = append(repr, []byte(fmt.Sprintf("#%s\n", line.Components[0]))...) + } else if line.Type == COLONCOMMENT { + repr = append(repr, []byte(fmt.Sprintf(";%s\n", line.Components[0]))...) + + // 4.assignments + } else if line.Type == ASSIGNMENT { + repr = append(repr, []byte(fmt.Sprintf("%s %s;\n", line.Components[0], line.Components[1]))...) + + // 5. include + } else if line.Type == INCLUDE { + repr = append(repr, []byte(fmt.Sprintf("include %s;\n", line.Components[0]))...) + + // 6. section + } else { + // push to stack + stack = append(stack, nil) + + // push children lines in reverse order + if len(line.Lines) > 0 { + for l := len(line.Lines); l > 0; l-- { + stack = append(stack, line.Lines[l]) + } + } + + repr = append(repr, []byte(fmt.Sprintf("%s {\n", line.Components[0]))...) + indent++ + + } + + _, err := e.writer.Write(repr) + if err != nil { + return err + } + + } + return nil } diff --git a/internal/cnf/parser/nginx/encoder_test.go b/internal/cnf/parser/nginx/encoder_test.go new file mode 100644 index 0000000..11a2cda --- /dev/null +++ b/internal/cnf/parser/nginx/encoder_test.go @@ -0,0 +1,102 @@ +package nginx + +import ( + "bytes" + "strings" + "testing" +) + +func TestDecodeEncode(t *testing.T) { + tests := []struct { + Input string + Output string + }{ + {"key value;\n", "key value;\n"}, + {"key value;\n", "key value;\n"}, + {"key \t value;\n", "key value;\n"}, + {"key\tvalue;\n", "key value;\n"}, + {"ke-y value;\n", "ke-y value;\n"}, + {"ke_y value;\n", "ke_y value;\n"}, + {"key value; \n", "key value;\n"}, + {"key value;\t\n", "key value;\n"}, + {"\tkey value;\n", "key value;\n"}, + {" \t key value;\n", "key value;\n"}, + + {"include ./file/*.conf;\n", "include ./file/*.conf;\n"}, + {"include ./file/*.conf; \n", "include ./file/*.conf;\n"}, + {"include ./file/*.conf;\t\n", "include ./file/*.conf;\n"}, + {"\tinclude ./file/*.conf;\n", "include ./file/*.conf;\n"}, + {" \t include ./file/*.conf;\n", "include ./file/*.conf;\n"}, + + {"sectionname {\n}\n", "sectionname {\n}\n\n"}, + {"section-name {\n}\n", "section-name {\n}\n\n"}, + {"section_name {\n}\n", "section_name {\n}\n\n"}, + {"sectionname { \n}\n", "sectionname {\n}\n\n"}, + {"sectionname {\t\n}\n", "sectionname {\n}\n\n"}, + {"\tsectionname {\n}\n", "sectionname {\n}\n\n"}, + {" \t sectionname {\n}\n", "sectionname {\n}\n\n"}, + + {"#some comment\n", "#some comment\n"}, + {"#some\tcomment\n", "#some\tcomment\n"}, + {"# some comment \n", "# some comment\n"}, + {"# some comment \t\n", "# some comment\n"}, + {"\t# some comment {\n", "# some comment {\n"}, + + {";some comment\n", ";some comment\n"}, + {"; some\tcomment\n", "; some\tcomment\n"}, + {"; some comment\n", "; some comment\n"}, + {"; some comment \n", "; some comment\n"}, + {"; some comment \t\n", "; some comment\n"}, + {"\t; some comment {\n", "; some comment {\n"}, + } + + for i, test := range tests { + + // create reader/writer + r, w := strings.NewReader(test.Input), &bytes.Buffer{} + + // parse input + receiver := new(nginx) + decoder := NewDecoder(r) + if err := decoder.Decode(receiver); err != nil { + t.Errorf("[%d] unexpected error <%s>", i, err) + continue + } + + // encode back to writer + encoder := NewEncoder(w) + encoder.SetIndent("", "\t") + if err := encoder.Encode(receiver); err != nil { + t.Errorf("[%d] unexpected error <%s>", i, err) + continue + } + + // check equality + if w.String() != test.Output { + t.Errorf("[%d] expected '%s', got '%s'", i, escape(test.Output), escape(w.String())) + } + + } +} + +func escape(raw string) string { + + escaped := make([]rune, 0) + + for _, char := range raw { + if char == '\n' { + escaped = append(escaped, []rune("\\n")...) + } else if char == '\t' { + escaped = append(escaped, []rune("\\t")...) + } else if char == '\r' { + escaped = append(escaped, []rune("\\r")...) + + } else { + escaped = append(escaped, char) + } + + } + + return string(escaped) + +} diff --git a/internal/cnf/parser/nginx/errors.go b/internal/cnf/parser/nginx/errors.go index d80bf7e..82503ee 100644 --- a/internal/cnf/parser/nginx/errors.go +++ b/internal/cnf/parser/nginx/errors.go @@ -5,6 +5,23 @@ import ( "git.xdrm.io/go/nix-amer/internal/clifmt" ) +// ErrNullReceiver is raised when a null receiver is provided +var ErrNullReceiver = fmt.Errorf("receiver must not be null") + +// ErrInvalidReceiver is raised when an invalid receiver is provided +var ErrInvalidReceiver = fmt.Errorf("receiver must be compatible with *[]*Line") + +// ErrUnexpectedSectionClose is raised when reading a section close '}' without an +// open section +var ErrUnexpectedSectionClose = fmt.Errorf("unexpected section close") + +// ErrUnclosedSection is raised when there is unclosed sections reaching the end +// of the file +var ErrUnclosedSection = fmt.Errorf("unclosed section") + +// ErrInvalidSyntax is raised when a line cannot be understood +var ErrInvalidSyntax = fmt.Errorf("invalid syntax") + // LineError wraps errors with a line index type LineError struct { Line int diff --git a/internal/cnf/parser/nginx/nginx.go b/internal/cnf/parser/nginx/nginx.go index 38249da..68127a4 100644 --- a/internal/cnf/parser/nginx/nginx.go +++ b/internal/cnf/parser/nginx/nginx.go @@ -9,10 +9,15 @@ type nginx struct { } // NewDecoder implements parser.T -func (n *nginx) NewDecoder(r io.Reader) *decoder { +func NewDecoder(r io.Reader) *decoder { return &decoder{reader: r} } +// NewEncoder implements parser.T +func NewEncoder(w io.Writer) *encoder { + return &encoder{writer: w} +} + // Section returns the section with a given name at the root level // nil is returned if no match found func (l *nginx) Section(name string) *Line {