From d71894a044cf36688efb892e84e5317946c3d9e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Gim=C3=A9nez?= Date: Sat, 11 Mar 2023 19:41:51 +0900 Subject: [PATCH] Super basic NIP file parser --- go.mod | 11 +++ pkg/nip/file_reader.go | 58 +++++++++++++ pkg/nip/nip_parser.go | 163 +++++++++++++++++++++++++++++++++++++ pkg/nip/nip_parser_test.go | 95 +++++++++++++++++++++ pkg/nip/rule.go | 34 ++++++++ 5 files changed, 361 insertions(+) create mode 100644 go.mod create mode 100644 pkg/nip/file_reader.go create mode 100644 pkg/nip/nip_parser.go create mode 100644 pkg/nip/nip_parser_test.go create mode 100644 pkg/nip/rule.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..665a708 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/hectorgimenez/d2go + +go 1.19 + +require github.com/stretchr/testify v1.8.2 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/pkg/nip/file_reader.go b/pkg/nip/file_reader.go new file mode 100644 index 0000000..fdc635e --- /dev/null +++ b/pkg/nip/file_reader.go @@ -0,0 +1,58 @@ +package pickit + +import ( + "bufio" + "errors" + "fmt" + "os" + "strings" +) + +func ReadDir(path string) ([]Rule, error) { + files, err := os.ReadDir(path) + if err != nil { + return nil, err + } + + rules := make([]Rule, 0) + for _, file := range files { + if !strings.HasSuffix(strings.ToLower(file.Name()), ".nip") || file.IsDir() { + continue + } + + newRules, err := ParseNIPFile(path + file.Name()) + if err != nil { + return nil, err + } + + rules = append(rules, newRules...) + } + + return rules, nil +} + +func ParseNIPFile(filePath string) ([]Rule, error) { + fileReader, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer fileReader.Close() + + fileScanner := bufio.NewScanner(fileReader) + fileScanner.Split(bufio.ScanLines) + + rules := make([]Rule, 0) + for fileScanner.Scan() { + rule, err := parseLine(fileScanner.Text()) + if errors.Is(err, errEmptyLine) { + continue + } + if err != nil { + return nil, fmt.Errorf("error reading file: %w", err) + } + + rules = append(rules, rule) + } + + return rules, nil +} diff --git a/pkg/nip/nip_parser.go b/pkg/nip/nip_parser.go new file mode 100644 index 0000000..830a3dd --- /dev/null +++ b/pkg/nip/nip_parser.go @@ -0,0 +1,163 @@ +package pickit + +import ( + "errors" + "regexp" + "strconv" + "strings" +) + +var ( + errEmptyLine = errors.New("empty line") + operandRegex = regexp.MustCompile("(\\|{2}|\\&{2})") + comparableRegex = regexp.MustCompile("(\\={2}|\\<\\=|\\>\\=|\\>|\\<|\\!\\=)") + propertyNameRegex = regexp.MustCompile("\\[(.*)\\]") +) + +func parseLine(line string) (Rule, error) { + line = lineCleanup(line) + if line == "" { + return Rule{}, errEmptyLine + } + + lineSplit := strings.Split(line, "#") + properties, err := buildGroups(lineSplit[0]) + if err != nil { + return Rule{}, err + } + + var stats []Group + if len(lineSplit) > 1 { + stats, err = buildGroups(lineSplit[1]) + if err != nil { + return Rule{}, err + } + } + + var maxQuantity []Group + if len(lineSplit) > 2 { + maxQuantity, err = buildGroups(lineSplit[2]) + if err != nil { + return Rule{}, err + } + } + + return Rule{ + Properties: properties, + Stats: stats, + MaxQuantity: maxQuantity, + }, nil +} + +func lineCleanup(line string) string { + l := strings.Split(line, "//") + line = strings.TrimSpace(l[0]) + + return strings.ToLower(line) +} + +func buildGroups(block string) ([]Group, error) { + operands := operandRegex.FindAllString(block, -1) + groupLines := operandRegex.Split(block, -1) + + operands, groupLines = reGroupParentheses(operands, groupLines) + + if len(operands) != len(groupLines)-1 { + return nil, errors.New("syntax error") + } + + groups := make([]Group, 0) + for i, groupLine := range groupLines { + cmp, err := buildGroupComparables(groupLine) + if err != nil { + return nil, err + } + + group := Group{Comparable: cmp} + if i < len(operands) { + group.Operand = Operand(operands[i]) + } + + groups = append(groups, group) + } + + return groups, nil +} + +func reGroupParentheses(operands, groups []string) ([]string, []string) { + cleanOperands := make([]string, 0) + cleanGroups := make([]string, 0) + + inGroup := false + currentGroupContent := "" + for i, group := range groups { + group = strings.TrimSpace(group) + if strings.Contains(group, "(") || (inGroup && !strings.Contains(group, ")")) { + inGroup = true + currentGroupContent += strings.ReplaceAll(group, "(", "") + operands[i] + continue + } + if strings.Contains(group, ")") { + inGroup = false + currentGroupContent += strings.ReplaceAll(group, ")", "") + cleanGroups = append(cleanGroups, currentGroupContent) + currentGroupContent = "" + if i < len(operands) { + cleanOperands = append(cleanOperands, operands[i]) + } + continue + } + if !inGroup { + cleanGroups = append(cleanGroups, group) + if i < len(operands) { + cleanOperands = append(cleanOperands, operands[i]) + } + } + } + + return cleanOperands, cleanGroups +} + +func buildGroupComparables(group string) ([]Comparable, error) { + operands := operandRegex.FindAllString(group, -1) + properties := operandRegex.Split(group, -1) + if len(properties)-1 != len(operands) { + return nil, errors.New("invalid NIP block") + } + + comparables := make([]Comparable, 0) + for i, prop := range properties { + comparisonSymbol := comparableRegex.FindString(prop) + values := comparableRegex.Split(prop, -1) + if len(values) != 2 { + return nil, errors.New("invalid NIP line") + } + + aggregatedProperties := strings.Split(values[0], "+") + for propNum, prop := range aggregatedProperties { + propertyNameGr := propertyNameRegex.FindStringSubmatch(prop) + propertyName := strings.TrimSpace(propertyNameGr[1]) + + cmp := Comparable{ + Keyword: propertyName, + Comparison: Operand(strings.TrimSpace(comparisonSymbol)), + } + if len(aggregatedProperties) > 0 && propNum < len(aggregatedProperties)-1 { + cmp.Operand = OperandOr + } else if i < len(operands) { + cmp.Operand = Operand(operands[i]) + } + + value := strings.TrimSpace(values[1]) + if val, err := strconv.Atoi(value); err == nil { + cmp.ValueInt = val + } else { + cmp.ValueString = value + } + + comparables = append(comparables, cmp) + } + } + + return comparables, nil +} diff --git a/pkg/nip/nip_parser_test.go b/pkg/nip/nip_parser_test.go new file mode 100644 index 0000000..b1aaf0a --- /dev/null +++ b/pkg/nip/nip_parser_test.go @@ -0,0 +1,95 @@ +package pickit + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +const nipLine = "[type] == boots && [quality] == rare # [frw] >= 10 && [fireresist] >= 10 && ([lightresist]+[coldresist] >= 10 && [dexterity] >= 1 && [fireresist]+[poisonresist] >= 10) // this is a comment" + +func Test_parseLine(t *testing.T) { + rules, err := parseLine(nipLine) + require.NoError(t, err) + + expected := Rule{ + Properties: []Group{ + { + Comparable: []Comparable{ + { + Keyword: "type", + Comparison: OperandEqual, + ValueString: "boots", + }, + }, + Operand: OperandAnd, + }, + { + Comparable: []Comparable{ + { + Keyword: "quality", + Comparison: OperandEqual, + ValueString: "rare", + }, + }, + }, + }, + Stats: []Group{ + { + Comparable: []Comparable{ + { + Keyword: "frw", + Comparison: OperandGreaterOrEqualTo, + ValueInt: 10, + }, + }, + Operand: OperandAnd, + }, + { + Comparable: []Comparable{ + { + Keyword: "fireresist", + Comparison: OperandGreaterOrEqualTo, + ValueInt: 10, + }, + }, + Operand: OperandAnd, + }, + { + Comparable: []Comparable{ + { + Keyword: "lightresist", + Comparison: OperandGreaterOrEqualTo, + ValueInt: 10, + Operand: OperandOr, + }, + { + Keyword: "coldresist", + Comparison: OperandGreaterOrEqualTo, + ValueInt: 10, + Operand: OperandAnd, + }, + { + Keyword: "dexterity", + Comparison: OperandGreaterOrEqualTo, + ValueInt: 1, + Operand: OperandAnd, + }, + { + Keyword: "fireresist", + Comparison: OperandGreaterOrEqualTo, + ValueInt: 10, + Operand: OperandOr, + }, + { + Keyword: "poisonresist", + Comparison: OperandGreaterOrEqualTo, + ValueInt: 10, + }, + }, + }, + }, + } + + assert.Equal(t, expected, rules) +} diff --git a/pkg/nip/rule.go b/pkg/nip/rule.go new file mode 100644 index 0000000..48589d3 --- /dev/null +++ b/pkg/nip/rule.go @@ -0,0 +1,34 @@ +package pickit + +const ( + OperandEqual Operand = "==" + OperandGreaterThan Operand = ">" + OperandGreaterOrEqualTo Operand = ">=" + OperandLessThan Operand = "<" + OperandLessThanOrEqualTo Operand = "<=" + OperandNotEqualTo Operand = "!=" + OperandAnd Operand = "&&" + OperandOr Operand = "||" +) + +type Rule struct { + Properties []Group + Stats []Group + MaxQuantity []Group +} + +type Keyword string +type Operand string + +type Comparable struct { + Keyword string + Comparison Operand + ValueInt int + ValueString string + Operand +} + +type Group struct { + Comparable []Comparable + Operand +}