update nginx/decoder (working for simplest case) | add tests for all line types + nested sections

This commit is contained in:
xdrm-brackets 2018-11-12 12:00:41 +01:00
parent 08b8cf8493
commit 194b5647ee
4 changed files with 164 additions and 32 deletions

View File

@ -8,8 +8,11 @@ import (
"strings" "strings"
) )
var ErrNullReceiver = fmt.Errorf("decoder receiver must not be null") var ErrNullReceiver = fmt.Errorf("receiver must not be null")
var ErrInvalidReceiver = fmt.Errorf("decoder receiver must be compatible with *[]*Line") var ErrInvalidReceiver = fmt.Errorf("receiver must be compatible with *[]*Line")
var ErrUnexpectedSectionClose = fmt.Errorf("unexpected section close")
var ErrUnclosedSection = fmt.Errorf("unclosed section")
var ErrInvalidSyntax = fmt.Errorf("invalid syntax")
// decoder implements parser.Decoder // decoder implements parser.Decoder
type decoder struct{ reader io.Reader } type decoder struct{ reader io.Reader }
@ -20,21 +23,22 @@ func (d *decoder) Decode(v interface{}) error {
if v == nil { if v == nil {
return ErrNullReceiver return ErrNullReceiver
} }
vcast, ok := v.(*[]*Line) vcast, ok := v.(*nginx)
if !ok { if !ok {
return ErrInvalidReceiver return ErrInvalidReceiver
} }
vcast.Lines = make([]*Line, 0)
r := bufio.NewReader(d.reader) r := bufio.NewReader(d.reader)
n := -1 // line number n := -1 // line number
// regexes // regexes
reSection := regexp.MustCompile(`(?m)^([a-z0-9_-]+)\s+\{$`) reSection := regexp.MustCompile(`(?m)^([a-z0-9_-]+)\s*\{$`)
reAssign := regexp.MustCompile(`(?m)^([a-z0-9_-]+)\s+([^;#]+);$`) reAssign := regexp.MustCompile(`(?m)^([a-z0-9_-]+)\s+([^;#]+);$`)
reInclude := regexp.MustCompile(`(?m)^include\s+([^;#]+);$`) reInclude := regexp.MustCompile(`(?m)^include\s+([^;#]+);$`)
// actual section tree // actual section stack
// tree := []Line{} stack := make([]*Line, 0)
for { for {
n++ n++
@ -46,18 +50,19 @@ func (d *decoder) Decode(v interface{}) error {
if err == io.EOF { if err == io.EOF {
break break
} else if err != nil { } else if err != nil {
return err return &LineError{n, err}
} }
notrim = strings.Trim(notrim, "\r\n")
// 2. ignore empty // 2. ignore empty
line := strings.Trim(notrim, " \t\r\n") line := strings.Trim(notrim, " \t")
if len(line) < 1 { if len(line) < 1 {
continue continue
} }
// 3. get indentation // 3. get indentation
firstChar := 0 var firstChar int
for ; !in_array(" \t\r\n", notrim[firstChar:][0]); firstChar++ { for firstChar = 0; in_array(" \t", notrim[firstChar]); firstChar++ {
} }
l.Indent = notrim[0:firstChar] l.Indent = notrim[0:firstChar]
@ -68,6 +73,11 @@ func (d *decoder) Decode(v interface{}) error {
} else if line[0] == ';' { } else if line[0] == ';' {
l.Type = COLONCOMMENT l.Type = COLONCOMMENT
l.Components = []string{strings.Trim(line[1:], " \t")} l.Components = []string{strings.Trim(line[1:], " \t")}
} else if line[0] == '}' {
l.Type = SECTIONEND
if len(stack) < 1 {
return &LineError{n, ErrUnexpectedSectionClose}
}
} }
// 4. section // 4. section
@ -76,6 +86,8 @@ func (d *decoder) Decode(v interface{}) error {
if match != nil { if match != nil {
l.Type = SECTION l.Type = SECTION
l.Components = match[1:] l.Components = match[1:]
l.Lines = make([]*Line, 0)
stack = append(stack, l)
} }
} }
@ -97,8 +109,45 @@ func (d *decoder) Decode(v interface{}) error {
} }
} }
*vcast = append(*vcast, l) // 7. invalid type
if l.Type == NONE {
return &LineError{n, ErrInvalidSyntax}
}
// 8. pop section
if l.Type == SECTIONEND {
cur := stack[len(stack)-1] // pop
stack = stack[0 : len(stack)-1] // pop
var last []*Line
if len(stack) > 0 {
last = stack[len(stack)-1].Lines
} else {
last = vcast.Lines
}
last = append(last, cur)
continue
}
// 9. add to section / or receiver
stacklen := len(stack)
if stacklen > 0 && l.Type == SECTION {
stacklen--
}
if stacklen > 0 {
tail := stack[stacklen-1]
tail.Lines = append(tail.Lines, l)
} else {
vcast.Lines = append(vcast.Lines, l)
}
}
// fail on unclosed section
if len(stack) > 0 {
return ErrUnclosedSection
} }
return nil return nil

View File

@ -29,13 +29,13 @@ func TestEachLineType(t *testing.T) {
{"\tinclude ./file/*.conf;\n", []string{"./file/*.conf"}, INCLUDE}, {"\tinclude ./file/*.conf;\n", []string{"./file/*.conf"}, INCLUDE},
{" \t include ./file/*.conf;\n", []string{"./file/*.conf"}, INCLUDE}, {" \t include ./file/*.conf;\n", []string{"./file/*.conf"}, INCLUDE},
{"sectionname {\n", []string{"sectionname"}, SECTION}, {"sectionname {\n}\n", []string{"sectionname"}, SECTION},
{"section-name {\n", []string{"section-name"}, SECTION}, {"section-name {\n}\n", []string{"section-name"}, SECTION},
{"section_name {\n", []string{"section_name"}, SECTION}, {"section_name {\n}\n", []string{"section_name"}, SECTION},
{"sectionname { \n", []string{"sectionname"}, SECTION}, {"sectionname { \n}\n", []string{"sectionname"}, SECTION},
{"sectionname {\t\n", []string{"sectionname"}, SECTION}, {"sectionname {\t\n}\n", []string{"sectionname"}, SECTION},
{"\tsectionname {\n", []string{"sectionname"}, SECTION}, {"\tsectionname {\n}\n", []string{"sectionname"}, SECTION},
{" \t sectionname {\n", []string{"sectionname"}, SECTION}, {" \t sectionname {\n}\n", []string{"sectionname"}, SECTION},
{"#some comment\n", []string{"some comment"}, COMMENT}, {"#some comment\n", []string{"some comment"}, COMMENT},
{"#some\tcomment\n", []string{"some\tcomment"}, COMMENT}, {"#some\tcomment\n", []string{"some\tcomment"}, COMMENT},
@ -58,41 +58,106 @@ func TestEachLineType(t *testing.T) {
decoder := parser.NewDecoder(strings.NewReader(test.Raw)) decoder := parser.NewDecoder(strings.NewReader(test.Raw))
// 2. Decode // 2. Decode
receiver := []*Line{} receiver := new(nginx)
err := decoder.Decode(&receiver) err := decoder.Decode(receiver)
if err != nil { if err != nil {
t.Errorf("[%d] unexpected error <%s>", i, err) t.Errorf("[%d] unexpected error <%s>", i, err)
continue continue
} }
if len(receiver) != 1 { if len(receiver.Lines) != 1 {
t.Errorf("[%d] expected only 1 element, got %d", i, len(receiver)) t.Errorf("[%d] expected only 1 element, got %d", i, len(receiver.Lines))
continue continue
} }
if receiver[0].Type != test.Type { if receiver.Lines[0].Type != test.Type {
t.Errorf("[%d] expected type %d, got %d", i, test.Type, receiver[0].Type) t.Errorf("[%d] expected type %d, got %d", i, test.Type, receiver.Lines[0].Type)
continue continue
} }
if receiver[0].Components == nil && test.Components != nil { if receiver.Lines[0].Components == nil && test.Components != nil {
t.Errorf("[%d] expected components not to be null", i) t.Errorf("[%d] expected components not to be null", i)
continue continue
} }
if len(receiver[0].Components) != len(test.Components) { if len(receiver.Lines[0].Components) != len(test.Components) {
t.Errorf("[%d] expected %d components, got %d", i, len(test.Components), len(receiver[0].Components)) t.Errorf("[%d] expected %d components, got %d", i, len(test.Components), len(receiver.Lines[0].Components))
continue continue
} }
// check each component individually // check each component individually
for c, comp := range receiver[0].Components { for c, comp := range receiver.Lines[0].Components {
if comp != test.Components[c] { if comp != test.Components[c] {
t.Errorf("[%d] expected component %d to be '%s', got '%s'", i, c, test.Components[c], comp) t.Errorf("[%d] expected component %d to be '%s', got '%s'", i, c, test.Components[c], comp)
continue
} }
} }
} }
} }
func TestNestedSections(t *testing.T) {
tests := []struct {
Raw string
SectionChain []string
}{
{"a{\n}\n", []string{"a"}},
{"a{\n\n}\n", []string{"a"}},
{"a{\n; some comment\n}\n", []string{"a"}},
{"a{\n; some comment\n #another comment\n}\n", []string{"a"}},
{"a{\nb{\n}\n}\n", []string{"a", "b"}},
{"a{\n\tb{\n\t}\n}\n", []string{"a", "b"}},
{"a{\n\tb{\n\t\tc{\n\t\t}\n\t}\n}\n", []string{"a", "b", "c"}},
}
for i, test := range tests {
// 1. create reader
parser := new(nginx)
decoder := parser.NewDecoder(strings.NewReader(test.Raw))
// 2. Decode
receiver := new(nginx)
err := decoder.Decode(receiver)
if err != nil {
t.Errorf("[%d] unexpected error <%s>", i, err)
continue
}
if len(receiver.Lines) != 1 {
t.Errorf("[%d] expected only 1 element, got %d", i, len(receiver.Lines))
continue
}
if receiver.Lines[0].Type != SECTION {
t.Errorf("[%d] expected type %d, got %d", i, SECTION, receiver.Lines[0].Type)
continue
}
// check each component individually
var current []*Line = receiver.Lines
for s, sec := range test.SectionChain {
// check that section exists in <current>
found := false
for _, line := range current {
// found
if line.Type == SECTION && line.Components[0] == sec {
found = true
current = line.Lines
break
}
}
if !found {
t.Errorf("[%d] key '%s' not found\n", i, strings.Join(test.SectionChain[:s+1], "/"))
break
}
}
}
}

View File

@ -0,0 +1,17 @@
package nginx
import (
"fmt"
"git.xdrm.io/go/nix-amer/internal/clifmt"
)
// LineError wraps errors with a line index
type LineError struct {
Line int
Err error
}
// Error implements Error
func (le LineError) Error() string {
return fmt.Sprintf(":%d %s", le.Line, clifmt.Color(31, le.Err.Error()))
}

View File

@ -13,6 +13,7 @@ const (
ASSIGNMENT ASSIGNMENT
INCLUDE INCLUDE
SECTION SECTION
SECTIONEND
) )
type Line struct { type Line struct {
@ -30,8 +31,8 @@ type Line struct {
// Components of the line // Components of the line
Components []string Components []string
// Children of the current section (nil if not a section) // Lines children of the current section (nil if not a section)
Children []Line Lines []*Line
// Indent is the indentation characters // Indent is the indentation characters
Indent string Indent string