Skip to content
Open
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