Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Refactor graph_connect.go for better readability #69

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
3 changes: 2 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ linters:
- goconst
- goheader
- goimports
- gomnd
- gomodguard
- goprintffuncname
- ineffassign
Expand Down Expand Up @@ -394,7 +395,6 @@ linters:
- gocyclo
- godox
- goerr113
- gomnd
- nestif
- nlreturn
- structcheck
Expand Down Expand Up @@ -424,6 +424,7 @@ issues:
- errcheck
# - dupl
- gosec
- funlen

# Exclude known linters from partially hard-vendored code,
# which is impossible to exclude via "nolint" comments.
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# GoFlow - Dataflow and Flow-based programming library for Go (golang)

[![Build Status](https://travis-ci.com/trustmaster/goflow.svg?branch=master)](https://travis-ci.com/trustmaster/goflow) [![codecov](https://codecov.io/gh/trustmaster/goflow/branch/master/graph/badge.svg)](https://codecov.io/gh/trustmaster/goflow)
[![Build Status](https://travis-ci.com/trustmaster/goflow.svg?branch=master)](https://travis-ci.com/trustmaster/goflow)
[![codecov](https://codecov.io/gh/trustmaster/goflow/branch/master/graph/badge.svg)](https://codecov.io/gh/trustmaster/goflow)
[![PkgGoDev](https://pkg.go.dev/badge/github.com/trustmaster/goflow)](https://pkg.go.dev/github.com/trustmaster/goflow)


### _Status of this branch (WIP)_
Expand Down
132 changes: 132 additions & 0 deletions address.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package goflow

import (
"fmt"
"strconv"
"strings"
"unicode"
)

// address is a full port accessor including the index part.
type address struct {
proc string // Process name
port string // Component port name
key string // Port key (only for map ports)
index int // Port index (only for array ports)
}

// noIndex is a "zero" index value. Not a `0` since 0 is a valid array index.
const noIndex = -1

type portKind uint

const (
portKindNone portKind = iota
portKindChan
portKindArray
portKindMap
)

func (a address) kind() portKind {
switch {
case len(a.proc) == 0 || len(a.port) == 0:
return portKindNone
case a.index != noIndex:
return portKindArray
case len(a.key) != 0:
return portKindMap
default:
return portKindChan
}
}

func (a address) String() string {
switch a.kind() {
case portKindChan:
return fmt.Sprintf("%s.%s", a.proc, a.port)
case portKindArray:
return fmt.Sprintf("%s.%s[%d]", a.proc, a.port, a.index)
case portKindMap:
return fmt.Sprintf("%s.%s[%s]", a.proc, a.port, a.key)
case portKindNone: // makes go-lint happy
}

return "<none>"
}

// parseAddress validates and constructs a port address.
// port parameter may include an array index ("<port name>[<index>]") or a hashmap key ("<port name>[<key>]").
func parseAddress(proc, port string) (address, error) {
switch {
case len(proc) == 0:
return address{}, fmt.Errorf("empty process name")
case len(port) == 0:
return address{}, fmt.Errorf("empty port name")
}

// Validate the proc contents
for i, r := range proc {
if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
return address{}, fmt.Errorf("unexpected %q at process name index %d", r, i)
}
}

keyPos := 0
a := address{
proc: proc,
port: port,
index: noIndex,
}

// Validate and parse the port contents in one scan
for i, r := range port {
switch {
case r == '[':
if i == 0 || keyPos > 0 {
// '[' at the very beginning of the port or a second '[' found
return address{}, fmt.Errorf("unexpected '[' at port name index %d", i)
}

keyPos = i + 1
a.port = port[:i]
case r == ']':
switch {
case keyPos == 0:
// No preceding matching '['
return address{}, fmt.Errorf("unexpected ']' at port name index %d", i)
case i != len(port)-1:
// Closing bracket is not the last rune
return address{}, fmt.Errorf("unexpected %q at port name index %d", port[i+1:], i)
}

if idx, err := strconv.Atoi(port[keyPos:i]); err != nil {
a.key = port[keyPos:i]
} else {
a.index = idx
}
case !unicode.IsLetter(r) && !unicode.IsDigit(r):
return address{}, fmt.Errorf("unexpected %q at port name index %d", r, i)
}
}

if keyPos != 0 && len(a.key) == 0 && a.index == noIndex {
return address{}, fmt.Errorf("unmatched '[' at port name index %d", keyPos-1)
}

a.port = capitalizePortName(a.port)

return a, nil
}

// capitalizePortName converts port names defined in UPPER or lower case to Title case,
// which is more common for structs in Go.
func capitalizePortName(name string) string {
lower := strings.ToLower(name)
upper := strings.ToUpper(name)

if name == lower || name == upper {
return strings.Title(lower)
}

return name
}
188 changes: 188 additions & 0 deletions address_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package goflow

import "testing"

func Test_address_kind(t *testing.T) {
type fields struct {
proc string
port string
key string
index int
}

tests := []struct {
name string
fields fields
want portKind
}{
{
name: "empty",
fields: fields{proc: "", port: "", key: "", index: noIndex},
want: portKindNone,
},
{
name: "no port name",
fields: fields{proc: "echo", port: "", key: "", index: noIndex},
want: portKindNone,
},
{
name: "no proc name",
fields: fields{proc: "", port: "in", key: "", index: noIndex},
want: portKindNone,
},
{
name: "chan port",
fields: fields{proc: "echo", port: "in", key: "", index: noIndex},
want: portKindChan,
},
{
name: "map port",
fields: fields{proc: "echo", port: "in", key: "key", index: noIndex},
want: portKindMap,
},
{
name: "array port",
fields: fields{proc: "echo", port: "in", key: "", index: 10},
want: portKindArray,
},
}

for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
a := address{
proc: tt.fields.proc,
port: tt.fields.port,
key: tt.fields.key,
index: tt.fields.index,
}
if got := a.kind(); got != tt.want {
t.Errorf("address.kind() = %v, want %v", got, tt.want)
}
})
}
}

func Test_address_String(t *testing.T) {
type fields struct {
proc string
port string
key string
index int
}

tests := []struct {
name string
fields fields
want string
}{
{
name: "empty",
fields: fields{proc: "", port: "", key: "", index: noIndex},
want: "<none>",
},
{
name: "no port name",
fields: fields{proc: "echo", port: "", key: "", index: noIndex},
want: "<none>",
},
{
name: "no proc name",
fields: fields{proc: "", port: "in", key: "", index: noIndex},
want: "<none>",
},
{
name: "chan port",
fields: fields{proc: "echo", port: "in", key: "", index: noIndex},
want: "echo.in",
},
{
name: "map port",
fields: fields{proc: "echo", port: "in", key: "key", index: noIndex},
want: "echo.in[key]",
},
{
name: "array port",
fields: fields{proc: "echo", port: "in", key: "", index: 10},
want: "echo.in[10]",
},
}

for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
a := address{
proc: tt.fields.proc,
port: tt.fields.port,
key: tt.fields.key,
index: tt.fields.index,
}
if got := a.String(); got != tt.want {
t.Errorf("address.String() = %v, want %v", got, tt.want)
}
})
}
}

func Test_parseAddress(t *testing.T) {
type args struct {
proc string
port string
}

tests := []struct {
name string
args args
want address
wantErr bool
}{
{name: "empty", args: args{proc: "", port: ""}, want: address{}, wantErr: true},
{name: "empty proc", args: args{proc: "", port: "in"}, want: address{}, wantErr: true},
{name: "empty port", args: args{proc: "echo", port: ""}, want: address{}, wantErr: true},
{name: "malformed port", args: args{proc: "echo", port: "in[[key1]"}, want: address{}, wantErr: true},
{name: "unmatched opening bracket", args: args{proc: "echo", port: "in[3"}, want: address{}, wantErr: true},
{name: "unmatched closing bracket", args: args{proc: "echo", port: "in]3"}, want: address{}, wantErr: true},
{name: "chars after closing bracket", args: args{proc: "echo", port: "in[3]abc"}, want: address{}, wantErr: true},
{name: "non-UTF-8 in proc", args: args{proc: "echo\xbd", port: "in"}, want: address{}, wantErr: true},
{name: "non-UTF-8 in port", args: args{proc: "echo", port: "in\xb2"}, want: address{}, wantErr: true},
{
name: "chan port",
args: args{proc: "echo", port: "in1"},
want: address{proc: "echo", port: "In1", key: "", index: noIndex},
wantErr: false,
},
{
name: "non-Latin chan port",
args: args{proc: "эхо", port: "ввод"},
want: address{proc: "эхо", port: "Ввод", key: "", index: noIndex},
wantErr: false,
},
{
name: "map port",
args: args{proc: "echo", port: "in[key1]"},
want: address{proc: "echo", port: "In", key: "key1", index: noIndex},
wantErr: false,
},
{
name: "array port",
args: args{proc: "echo", port: "in[10]"},
want: address{proc: "echo", port: "In", key: "", index: 10},
wantErr: false,
},
}

for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
got, err := parseAddress(tt.args.proc, tt.args.port)
if (err != nil) != tt.wantErr {
t.Errorf("parseAddress() error = %v, wantErr %v", err, tt.wantErr)
return
}

if got != tt.want {
t.Errorf("parseAddress() = %v, want %v", got, tt.want)
}
})
}
}
Loading