Skip to content

Commit

Permalink
Refactor and add comments to increase readability.
Browse files Browse the repository at this point in the history
Added test for parsing of input parameters
  • Loading branch information
johnerikhalse committed Jul 31, 2023
1 parent 169ce2e commit 6670b4f
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 9 deletions.
92 changes: 92 additions & 0 deletions cmd/report/query_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package report

import (
"github.com/stretchr/testify/assert"
"testing"
)

func Test_queryCmdOptions_parseQuery(t *testing.T) {
tests := []struct {
name string
fieldsFromFlags *queryCmdOptions
fieldsAfterParse *queryCmdOptions
want *query
wantErr assert.ErrorAssertionFunc
}{
{"template file",
&queryCmdOptions{queryOrFile: "testdata/template1.yaml"},
&queryCmdOptions{
queryOrFile: "testdata/template1.yaml",
},
&query{
Name: "template1",
Description: "Example template\n",
Query: "r.db('veidemann').table('config_crawl_entities')\n",
Template: "{{.id}} {{.meta.name}}\n",
},
assert.NoError},
{"template file with template flag",
&queryCmdOptions{
queryOrFile: "testdata/template1.yaml",
goTemplate: "{{.id}}",
},
&queryCmdOptions{
queryOrFile: "testdata/template1.yaml",
goTemplate: "{{.id}}",
},
&query{
Name: "template1",
Description: "Example template\n",
Query: "r.db('veidemann').table('config_crawl_entities')\n",
Template: "{{.id}} {{.meta.name}}\n",
},
assert.NoError},
{"template file with format flag",
&queryCmdOptions{
queryOrFile: "testdata/template1.yaml",
format: "yaml",
},
&queryCmdOptions{
queryOrFile: "testdata/template1.yaml",
format: "yaml",
},
&query{
Name: "template1",
Description: "Example template\n",
Query: "r.db('veidemann').table('config_crawl_entities')\n",
Template: "{{.id}} {{.meta.name}}\n",
},
assert.NoError},
{"nonexisting template file",
&queryCmdOptions{
queryOrFile: "missing.yaml",
},
&queryCmdOptions{queryOrFile: "missing.yaml"},
nil,
assert.Error},
{"query",
&queryCmdOptions{
queryOrFile: "r.db('veidemann').table('config_crawl_entities')",
},
&queryCmdOptions{
queryOrFile: "r.db('veidemann').table('config_crawl_entities')",
},
&query{
Name: "",
Description: "",
Query: "r.db('veidemann').table('config_crawl_entities')",
Template: "",
},
assert.NoError},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.fieldsFromFlags.parseQuery()
if !tt.wantErr(t, err, "parseQuery()") {
return
}
assert.Equalf(t, tt.fieldsAfterParse, tt.fieldsFromFlags, "Fields after parseQuery()")
assert.Equalf(t, tt.want, got, "parseQuery()")
})
}
}
8 changes: 8 additions & 0 deletions cmd/report/testdata/template1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
description: |
Example template
query: |
r.db('veidemann').table('config_crawl_entities')
template: |
{{.id}} {{.meta.name}}
39 changes: 30 additions & 9 deletions format/formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/nlnwa/veidemann-api/go/config/v1"
"github.com/rs/zerolog/log"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"io"
"os"
"strings"
Expand All @@ -41,17 +42,27 @@ var res embed.FS
// templateDir is the directory where the templates are located
const templateDir = "res/"

// Formatter is the interface for formatters
// Formatter is the interface for formatters.
// Formatters should not be instantiated directly, but through the NewFormatter function.
type Formatter interface {
// WriteRecord writes a record to the formatter.
// The input record is guaranteed to be a protobuf message
// or the result of parsing a json formatted string into an interface (see https://pkg.go.dev/encoding/json#Unmarshal).
WriteRecord(interface{}) error
Close() error
}

type anyRecord struct {
// anyElement is a struct that can hold any type of element.
// It is used as input to the json unmarshaler.
type anyElement struct {
v interface{}
}

func (r *anyRecord) UnmarshalJSON(b []byte) error {
// UnmarshalJSON implements the encoding/json.Unmarshaler interface.
// The function uses encoding.json.Unmarshal to unmarshal the input.
// If the input is a map[string]interface{}, the map is traversed to find any RethinkDb dates
// which are converted to RFC3339 formatted strings.
func (r *anyElement) UnmarshalJSON(b []byte) error {
var i interface{}
err := json.Unmarshal(b, &i)
if err != nil {
Expand All @@ -74,7 +85,7 @@ func (r *anyRecord) UnmarshalJSON(b []byte) error {
return err
}

func (r *anyRecord) traverseMap(i *map[string]interface{}) {
func (r *anyElement) traverseMap(i *map[string]interface{}) {
for k, v := range *i {
if m, ok := v.(map[string]interface{}); ok {
if d, ok := r.formatDate(m); ok {
Expand All @@ -98,11 +109,13 @@ func getAsInt(v interface{}) (int, bool) {
}
}

// formatDate if i is recognized as a RethinkDb date, the date is returned as a RFC3339 formatted string
func (r *anyRecord) formatDate(i map[string]interface{}) (string, bool) {
// formatDate first checks if i is a RethinkDb date.
// If so, date is the result of converting the date to a RFC3339 formatted string and isDate is true.
// Otherwise, date is the empty string and isDate is false.
func (r *anyElement) formatDate(element map[string]interface{}) (date string, isDate bool) {
var year, month, day, hour, minute, second, nano, offset int

if dateTime, ok := i["dateTime"].(map[string]interface{}); !ok {
if dateTime, ok := element["dateTime"].(map[string]interface{}); !ok {
return "", false
} else {
if date, ok := dateTime["date"].(map[string]interface{}); !ok {
Expand Down Expand Up @@ -135,7 +148,7 @@ func (r *anyRecord) formatDate(i map[string]interface{}) (string, bool) {
}
}
}
if of, ok := i["offset"].(map[string]interface{}); !ok {
if of, ok := element["offset"].(map[string]interface{}); !ok {
return "", false
} else {
if offset, ok = getAsInt(of["totalSeconds"]); !ok {
Expand All @@ -155,15 +168,23 @@ type preFormatter struct {
formatter Formatter
}

// WriteRecord implements the Formatter interface.
// If the input record is a string, it is parsed as json and the result is passed to the wrapped formatter.
// If the input record is a protobuf message, it is passed to the wrapped formatter.
// Otherwise, an error is returned.
func (p *preFormatter) WriteRecord(record interface{}) error {
switch v := record.(type) {
case string:
var j anyRecord
var j anyElement
err := json.Unmarshal([]byte(v), &j)
if err != nil {
return fmt.Errorf("failed to parse json: %w", err)
}
record = j.v
case proto.Message:
// Do nothing, just pass through
default:
return fmt.Errorf("unsupported type '%T'", v)
}

return p.formatter.WriteRecord(record)
Expand Down

0 comments on commit 6670b4f

Please sign in to comment.