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