Super basic NIP file parser
This commit is contained in:
11
go.mod
Normal file
11
go.mod
Normal 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
58
pkg/nip/file_reader.go
Normal 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
163
pkg/nip/nip_parser.go
Normal 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
|
||||
}
|
||||
95
pkg/nip/nip_parser_test.go
Normal file
95
pkg/nip/nip_parser_test.go
Normal 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
34
pkg/nip/rule.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user