Skip to content

Commit

Permalink
Fixed errors with null values present
Browse files Browse the repository at this point in the history
Can now serialize jsonnode.Node values. Fields are sorted in a "smart"
way utilizing a topological sort. Data frames now only return
json.RawMessage values or time.Time values. This (currently) has the
downside of allowing string values to be interpreted numerically if the
contents of a string are numeric.
  • Loading branch information
retrodaredevil committed Apr 1, 2024
1 parent 0eefc38 commit b33cb9c
Show file tree
Hide file tree
Showing 15 changed files with 2,534 additions and 224 deletions.
31 changes: 31 additions & 0 deletions pkg/plugin/parsing/fieldsort/order.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package fieldsort

type Order struct {
data *graph[string]
vertices map[string]bool
}

func New() *Order {
return &Order{
data: &graph[string]{
edges: make(map[string][]string),
},
vertices: make(map[string]bool),
}
}

func (o *Order) State(ordering []string) {
var last *string
for _, element := range ordering {
o.vertices[element] = true
if last != nil {
o.data.addEdge(*last, element)
}

elementCopy := element
last = &elementCopy
}
}
func (o *Order) GetOrder() []string {
return o.data.topologicalSort(o.vertices)
}
26 changes: 26 additions & 0 deletions pkg/plugin/parsing/fieldsort/order_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package fieldsort

import (
"strings"
"testing"
)

func TestOrderRealistic(t *testing.T) {
order := New()
order.State([]string{"batteryVoltage", "dateMillis", "meta.name", "meta.displayName"})
order.State([]string{"batteryVoltage", "dateMillis", "meta"})
//println(strings.Join(order.GetOrder(), ", "))
}
func TestOrderSimple(t *testing.T) {
order := New()
order.State([]string{"a", "b", "c", "f"})
order.State([]string{"b", "d", "e"})
order.State([]string{"c", "e", "f"})
//order.State([]string{"c", "d"})
result := order.GetOrder()
//println(strings.Join(result, ", "))
if result[0] != "a" || result[1] != "b" || result[4] != "e" || result[5] != "f" {
// Note that the order of c and d here is undetermined, which is OK
t.Errorf("Incorrect result: %s", strings.Join(result, ", "))
}
}
36 changes: 36 additions & 0 deletions pkg/plugin/parsing/fieldsort/topsort.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package fieldsort

// inspiration from https://reintech.io/blog/topological-sorting-in-go

type graph[Key comparable] struct {
edges map[Key][]Key
}

func (g *graph[Key]) addEdge(u, v Key) {
g.edges[u] = append(g.edges[u], v)
}

func (g *graph[Key]) topologicalSortUtil(v Key, visited map[Key]bool, stack *[]Key) {
visited[v] = true

for _, u := range g.edges[v] {
if !visited[u] {
g.topologicalSortUtil(u, visited, stack)
}
}

*stack = append([]Key{v}, *stack...)
}

func (g *graph[Key]) topologicalSort(vertices map[Key]bool) []Key {
var stack []Key
visited := make(map[Key]bool)

for vertex := range vertices {
if !visited[vertex] {
g.topologicalSortUtil(vertex, visited, &stack)
}
}

return stack
}
81 changes: 81 additions & 0 deletions pkg/plugin/parsing/framemap/convert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package framemap

import (
"encoding/json"
"fmt"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/wildmountainfarms/wild-graphql-datasource/pkg/plugin/parsing/fieldsort"
"time"
)

func (node *frameNode) getAllFields() []string {
order := fieldsort.New()

for _, row := range node.rows {
order.State(row.FieldOrder)
}

return order.GetOrder()
}

func (node *frameNode) isTimeField(field string) bool {
for _, row := range node.rows {
if _, exists := row.TimeMap[field]; exists {
return true
}
if _, exists := row.FieldMap[field]; exists {
return false
}
}
return false
}
func (node *frameNode) createField(field string) *data.Field {
if node.isTimeField(field) {
var values []*time.Time
for _, row := range node.rows {
value, exists := row.TimeMap[field]
if exists {
values = append(values, value)
} else {
values = append(values, nil)
}
}
return data.NewField(field, node.labels, values)
} else {
var values []*json.RawMessage
for _, row := range node.rows {
value, exists := row.FieldMap[field]
if exists {
values = append(values, &value)
} else {
values = append(values, nil)
}
}
return data.NewField(field, node.labels, values)
}
}

func (f *FrameMap) ToFrames() []*data.Frame {
// create data frame response.
// For an overview on data frames and how grafana handles them:
// https://grafana.com/developers/plugin-tools/introduction/data-frames
// The goal here is to output a long format. If needed, prepare time series can transform it
// https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/transform-data/#prepare-time-series

// NOTE: The order of the frames here determines the order they appear in the legend in Grafana
// This is why we use a linkedhashmap.Map everywhere, as it maintains order.
var r []*data.Frame
frameMapIterator := f.data.Iterator()
for frameMapIterator.Next() {
node := frameMapIterator.Value()

frameName := fmt.Sprintf("response %v", node.labels)
frame := data.NewFrame(frameName)

for _, field := range node.getAllFields() {
frame.Fields = append(frame.Fields, node.createField(field))
}
r = append(r, frame)
}
return r
}
61 changes: 17 additions & 44 deletions pkg/plugin/parsing/framemap/framemap.go
Original file line number Diff line number Diff line change
@@ -1,75 +1,48 @@
package framemap

import (
"fmt"
"github.com/emirpasic/gods/v2/maps/linkedhashmap"
"github.com/grafana/grafana-plugin-sdk-go/data"
)

type frameAndLabels struct {
type frameNode struct {
labels data.Labels
// A map of field names to an array of the values of that given column
fieldMap *linkedhashmap.Map[string, any]
rows []*Row
}

func keyOfLabels(labels data.Labels) string {
return labels.String()
}

type FrameMap struct {
data *linkedhashmap.Map[string, frameAndLabels]
data *linkedhashmap.Map[string, *frameNode]
}

func New() *FrameMap {
return &FrameMap{
data: linkedhashmap.New[string, frameAndLabels](),
data: linkedhashmap.New[string, *frameNode](),
}
}

func (f *FrameMap) Get(labels data.Labels) (*linkedhashmap.Map[string, any], bool) {
func (f *FrameMap) getOrCreateFrameNode(labels data.Labels) *frameNode {
mapKey := keyOfLabels(labels)
values, exists := f.data.Get(mapKey)
if !exists {
return nil, false
if exists {
return values
}
node := &frameNode{
labels: labels,
}
return values.fieldMap, true
}
func (f *FrameMap) Put(labels data.Labels, fieldMap *linkedhashmap.Map[string, any]) {
mapKey := keyOfLabels(labels)
f.data.Put(
mapKey,
frameAndLabels{
labels: labels,
fieldMap: fieldMap,
},
node,
)
return node
}
func (f *FrameMap) NewRow(labels data.Labels) *Row {
node := f.getOrCreateFrameNode(labels)

func (f *FrameMap) ToFrames() []*data.Frame {
// create data frame response.
// For an overview on data frames and how grafana handles them:
// https://grafana.com/developers/plugin-tools/introduction/data-frames
// The goal here is to output a long format. If needed, prepare time series can transform it
// https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/transform-data/#prepare-time-series

// NOTE: The order of the frames here determines the order they appear in the legend in Grafana
// This is why we use a linkedhashmap.Map everywhere, as it maintains order.
var r []*data.Frame
frameMapIterator := f.data.Iterator()
for frameMapIterator.Next() {
frameAndLabels := frameMapIterator.Value()

frameName := fmt.Sprintf("response %v", frameAndLabels.labels)
frame := data.NewFrame(frameName)
fieldMapIterator := frameAndLabels.fieldMap.Iterator()
for fieldMapIterator.Next() {
key := fieldMapIterator.Key()
values := fieldMapIterator.Value()
frame.Fields = append(frame.Fields,
data.NewField(key, frameAndLabels.labels, values),
)
}
r = append(r, frame)
}
return r
row := newRow()
node.rows = append(node.rows, row)
return row
}
53 changes: 0 additions & 53 deletions pkg/plugin/parsing/framemap/framemap_test.go

This file was deleted.

20 changes: 20 additions & 0 deletions pkg/plugin/parsing/framemap/row.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package framemap

import (
"encoding/json"
"time"
)

type Row struct {
FieldOrder []string
FieldMap map[string]json.RawMessage
TimeMap map[string]*time.Time
}

func newRow() *Row {
row := Row{
FieldMap: map[string]json.RawMessage{},
TimeMap: map[string]*time.Time{},
}
return &row
}
Loading

0 comments on commit b33cb9c

Please sign in to comment.