Super basic NIP file parser

This commit is contained in:
Héctor Giménez
2023-03-11 19:41:51 +09:00
parent 2414f0a727
commit d71894a044
5 changed files with 361 additions and 0 deletions

11
go.mod Normal file
View File

@@ -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
)

58
pkg/nip/file_reader.go Normal file
View File

@@ -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
}

163
pkg/nip/nip_parser.go Normal file
View File

@@ -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
}

View File

@@ -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)
}

34
pkg/nip/rule.go Normal file
View File

@@ -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
}