diff --git a/stream/mts/meta/meta.go b/stream/mts/meta/meta.go new file mode 100644 index 00000000..8bd5365b --- /dev/null +++ b/stream/mts/meta/meta.go @@ -0,0 +1,145 @@ +/* +NAME + meta.go + +DESCRIPTION + See Readme.md + +AUTHOR + Saxon Nelson-Milton + +LICENSE + meta.go is Copyright (C) 2017-2019 the Australian Ocean Lab (AusOcean) + + It is free software: you can redistribute it and/or modify them + under the terms of the GNU General Public License as published by the + Free Software Foundation, either version 3 of the License, or (at your + option) any later version. + + It is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. + + You should have received a copy of the GNU General Public License + along with revid in gpl.txt. If not, see http://www.gnu.org/licenses. +*/ + +package mts + +import ( + "errors" + "strings" + "sync" +) + +const headSize = 4 + +const ( + majVer = 1 + minVer = 0 +) + +const ( + dataLenIdx1 = 2 + dataLenIdx2 = 3 +) + +var ( + errKeyAbsent = errors.New("Key does not exist in map") +) + +type Metadata struct { + mu sync.RWMutex + data map[string]string + enc []byte +} + +func NewMeta() *Metadata { + return &Metadata{ + data: make(map[string]string), + enc: []byte{ + 0x00, // Reserved byte + (majVer << 4) | minVer, // MS and LS versions + 0x00, // Data len byte1 + 0x00, // Data len byte2 + }, + } +} + +// Add adds metadata with key and val, if already exists return error +func (m *Metadata) Add(key, val string) { + m.mu.Lock() + m.data[key] = val + m.mu.Unlock() +} + +// All returns the a copy of the map containing the meta data +func (m *Metadata) All() map[string]string { + m.mu.Lock() + cpy := make(map[string]string) + for k, v := range m.data { + cpy[k] = v + } + m.mu.Unlock() + return cpy +} + +// Get returns the meta data for the passed key +func (m *Metadata) Get(key string) (string, error) { + m.mu.Lock() + val, ok := m.data[key] + m.mu.Unlock() + if !ok { + return "", errKeyAbsent + } + + return val, nil +} + +// Remove deletes a meta entry in the map and returns error if it doesn’t exist +func (m *Metadata) Delete(key string) error { + m.mu.Lock() + defer m.mu.Unlock() + if _, ok := m.data[key]; ok { + delete(m.data, key) + return nil + } + return errKeyAbsent +} + +// Encode takes the meta data map and encods into a byte slice with header +// describing the version, length of data and data in TSV format. +func (m *Metadata) Encode() []byte { + m.enc = m.enc[:headSize] + + // Iterate over map and append entries, only adding tab if we're not on the last entry + var i int + var entry string + for k, v := range m.data { + i++ + entry += k + "=" + v + if i < len(m.data) { + entry += "\t" + } + } + m.enc = append(m.enc, []byte(entry)...) + + // Calculate and set data length in encoded meta header. + dataLen := len(m.enc[headSize:]) + m.enc[dataLenIdx1] = byte(dataLen >> 8) + m.enc[dataLenIdx2] = byte(dataLen) + + return m.enc +} + +func ReadFrom(d []byte, key string) (string, error) { + entries := strings.Split(string(d), "\t") + for _, entry := range entries { + kv := strings.Split(entry, "=") + if kv[0] == key { + return kv[1], nil + } + } + return "", errors.New("could not find key in metadata") +} diff --git a/stream/mts/meta/meta_test.go b/stream/mts/meta/meta_test.go new file mode 100644 index 00000000..11003b9b --- /dev/null +++ b/stream/mts/meta/meta_test.go @@ -0,0 +1,150 @@ +/* +NAME + meta_test.go + +DESCRIPTION + See Readme.md + +AUTHOR + Saxon Nelson-Milton + +LICENSE + meta_test.go is Copyright (C) 2017-2019 the Australian Ocean Lab (AusOcean) + + It is free software: you can redistribute it and/or modify them + under the terms of the GNU General Public License as published by the + Free Software Foundation, either version 3 of the License, or (at your + option) any later version. + + It is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. + + You should have received a copy of the GNU General Public License + along with revid in gpl.txt. If not, see http://www.gnu.org/licenses. +*/ + +package mts + +import ( + "bytes" + "errors" + "reflect" + "testing" +) + +const ( + tstKey1 = "loc" + tstData1 = "a,b,c" + tstKey2 = "ts" + tstData2 = "12345678" + tstData3 = "d,e,f" +) + +// TestAddAndGet ensures that we can add metadata and then successfully get it. +func TestAddAndGet(t *testing.T) { + meta := NewMeta() + meta.Add(tstKey1, tstData1) + meta.Add(tstKey2, tstData2) + errors.New("Trying to delete map entry that doesn't exist") + if data, err := meta.Get(tstKey1); err != nil { + t.Errorf("Could not get data for key: loc: %v", err.Error()) + if data != tstData1 { + t.Errorf("Did not get expected data") + } + } + + if data, err := meta.Get(tstKey2); err != nil { + t.Errorf("Could not get data for key: ts: %v", err.Error()) + if data != tstData2 { + t.Errorf("Did not get expected data") + } + } +} + +// TestUpdate checks that we can use Meta.Add to actually update metadata +// if it already exists in the Meta map. +func TestUpdate(t *testing.T) { + meta := NewMeta() + meta.Add(tstKey1, tstData1) + meta.Add(tstKey1, tstData3) + + if data, err := meta.Get(tstKey1); err != nil { + t.Errorf("Did not expect err: %v", err.Error()) + if data != tstData2 { + t.Errorf("Data did not correctly update for key \"loc\"") + } + } +} + +// TestAll ensures we can get a correct map using Meta.All() after adding some data +func TestAll(t *testing.T) { + meta := NewMeta() + tstMap := map[string]string{ + tstKey1: tstData1, + tstKey2: tstData2, + } + + meta.Add(tstKey1, tstData1) + meta.Add(tstKey2, tstData2) + metaMap := meta.All() + + if !reflect.DeepEqual(metaMap, tstMap) { + t.Errorf("Map not correct. Got: %v, want: %v", metaMap, tstMap) + } +} + +// TestGetAbsentKey ensures that we get the expected error when we try to get with +// key that does not yet exist in the Meta map. +func TestGetAbsentKey(t *testing.T) { + meta := NewMeta() + + if _, err := meta.Get(tstKey1); err != errKeyAbsent { + t.Errorf("Did not get expected err: %v", errKeyAbsent.Error()) + } +} + +// TestDelete ensures we can remove a data entry in the Meta map. +func TestDelete(t *testing.T) { + meta := NewMeta() + meta.Add(tstKey1, tstData1) + if err := meta.Delete(tstKey1); err != nil { + t.Errorf("Did not expect error: %v", err.Error()) + } + if _, err := meta.Get(tstKey1); err != errKeyAbsent { + t.Errorf("Did not get expected err: %v", errKeyAbsent.Error()) + } +} + +// TestDeleteAbsentKey checks that we get an expected error when we try to delete +// an entry in the Meta map that doesn't exist. +func TestDeleteAbsentKey(t *testing.T) { + meta := NewMeta() + if err := meta.Delete(tstKey1); err != errKeyAbsent { + t.Errorf("Did not get expected err: %v", errKeyAbsent.Error()) + } +} + +// TestEncode checks that we're getting the correct byte slice from Meta.Encode(). +func TestEncode(t *testing.T) { + meta := NewMeta() + meta.Add(tstKey1, tstData1) + meta.Add(tstKey2, tstData2) + + dataLen := len(tstKey1+tstData1+tstKey2+tstData2) + 3 + expectedOut := []byte{ + 0x00, + 0x10, + byte(dataLen >> 8), + byte(dataLen), + } + expectedOut = append(expectedOut, []byte( + tstKey1+"="+tstData1+"\t"+ + tstKey2+"="+tstData2)...) + + got := meta.Encode() + if !bytes.Equal(expectedOut, got) { + t.Errorf("Did not get expected out. \nGot : %v \nwant: %v", got, expectedOut) + } +}