Refactor FileStore to JSONStore (#30)
Badges / coveralls (push) Successful in 31s

Reviewed-on: #30
Co-authored-by: Natercio Moniz <[email protected]>
This commit was merged in pull request #30.
This commit is contained in:
2026-09-06 18:42:58 +01:00
committed by natercio
parent e5961c0dff
commit 3c404699e5
6 changed files with 132 additions and 76 deletions
+2 -2
View File
@@ -113,7 +113,7 @@ func runWithIO(ctx context.Context, stdin io.Reader, stdout io.Writer) error {
// buildStore returns the Store implementation that BuildReport should use. // buildStore returns the Store implementation that BuildReport should use.
// When --state-file is empty an EphemeralStore is used so behaviour is // When --state-file is empty an EphemeralStore is used so behaviour is
// identical to pre-persistence runs. Otherwise a JSON-backed FileStore is // identical to pre-persistence runs. Otherwise a JSON-backed JSONStore is
// returned, wired to a platform-specific RecordSerializer. // returned, wired to a platform-specific RecordSerializer.
func buildStore(stateFile, platform string, figi *ofigi.Client) (internal.Store, error) { func buildStore(stateFile, platform string, figi *ofigi.Client) (internal.Store, error) {
if stateFile == "" { if stateFile == "" {
@@ -128,7 +128,7 @@ func buildStore(stateFile, platform string, figi *ofigi.Client) (internal.Store,
return nil, fmt.Errorf("unsupported platform for state persistence: %s", platform) return nil, fmt.Errorf("unsupported platform for state persistence: %s", platform)
} }
store, err := internal.NewFileStore(stateFile, platform, serializer) store, err := internal.NewJSONStore(stateFile, platform, serializer)
if err != nil { if err != nil {
return nil, fmt.Errorf("creating file store: %w", err) return nil, fmt.Errorf("creating file store: %w", err)
} }
@@ -9,33 +9,32 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"time" "time"
) )
// FileStore is a Store backed by a single JSON file on disk. The on-disk // JSONStore is a Store backed by a single JSON file on disk. The on-disk
// schema is the platform-agnostic State struct; per-broker Record data is // schema is the platform-agnostic State struct; per-broker Record data is
// encoded and decoded through the supplied RecordSerializer. // encoded and decoded through the supplied RecordSerializer.
type FileStore struct { type JSONStore struct {
filename string filename string
platform string platform string
serializer RecordSerializer serializer RecordSerializer
} }
// NewFileStore constructs a FileStore that reads from and writes to the given // NewJSONStore constructs a JSONStore that reads from and writes to the given
// filename. The FileStore does not keep an *os.File open — Load and Save each // filename. The JSONStore does not keep an *os.File open — Load and Save each
// open the file themselves — so callers do not need to close it. The platform // open the file themselves — so callers do not need to close it. The platform
// string is recorded into the saved state and validated on Load so a state // string is recorded into the saved state and validated on Load so a state
// file from a different broker cannot be loaded by mistake. // file from a different broker cannot be loaded by mistake.
func NewFileStore(filename string, platform string, serializer RecordSerializer) (*FileStore, error) { func NewJSONStore(filename string, platform string, serializer RecordSerializer) (*JSONStore, error) {
if filename == "" { if filename == "" {
return nil, fmt.Errorf("filename cannot be empty") return nil, fmt.Errorf("filename cannot be empty")
} }
if serializer == nil { if serializer == nil {
return nil, fmt.Errorf("serializer cannot be nil") return nil, fmt.Errorf("serializer cannot be nil")
} }
return &FileStore{ return &JSONStore{
filename: filename, filename: filename,
platform: platform, platform: platform,
serializer: serializer, serializer: serializer,
@@ -45,7 +44,7 @@ func NewFileStore(filename string, platform string, serializer RecordSerializer)
// Load reads the state file and reconstructs the per-symbol FillerQueue map. // Load reads the state file and reconstructs the per-symbol FillerQueue map.
// A missing file is not an error — it returns an empty map so the first run // A missing file is not an error — it returns an empty map so the first run
// against a new state file Just Works. // against a new state file Just Works.
func (fs *FileStore) Load(ctx context.Context) (map[string]*FillerQueue, error) { func (fs *JSONStore) Load(ctx context.Context) (map[string]*FillerQueue, error) {
data, err := os.ReadFile(fs.filename) data, err := os.ReadFile(fs.filename)
if err != nil { if err != nil {
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
@@ -96,7 +95,7 @@ func (fs *FileStore) Load(ctx context.Context) (map[string]*FillerQueue, error)
// Save serialises the queue map to disk. The write is done via a temp file in // Save serialises the queue map to disk. The write is done via a temp file in
// the same directory followed by an atomic rename so a crash mid-write cannot // the same directory followed by an atomic rename so a crash mid-write cannot
// leave a half-written state file. // leave a half-written state file.
func (fs *FileStore) Save(ctx context.Context, queue map[string]*FillerQueue) error { func (fs *JSONStore) Save(ctx context.Context, queue map[string]*FillerQueue) error {
state := State{ state := State{
Version: StateVersion, Version: StateVersion,
Platform: fs.platform, Platform: fs.platform,
@@ -124,16 +123,6 @@ func (fs *FileStore) Save(ctx context.Context, queue map[string]*FillerQueue) er
state.Queues[symbol] = persisted state.Queues[symbol] = persisted
} }
ext := filepath.Ext(fs.filename)
name, _ := strings.CutSuffix(fs.filename, ext)
backupFilename := name + "." + strconv.FormatInt(time.Now().UnixMilli(), 10) + ext
err := os.Rename(fs.filename, backupFilename)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("rename old state file: %w", err)
}
}
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
enc := json.NewEncoder(buf) enc := json.NewEncoder(buf)
enc.SetIndent("", " ") enc.SetIndent("", " ")
@@ -141,13 +130,37 @@ func (fs *FileStore) Save(ctx context.Context, queue map[string]*FillerQueue) er
return fmt.Errorf("encoding state: %w", err) return fmt.Errorf("encoding state: %w", err)
} }
dst, err := os.Create(fs.filename) tmp, err := os.CreateTemp(filepath.Dir(fs.filename), filepath.Base(fs.filename)+".*.tmp")
if err != nil { if err != nil {
return fmt.Errorf("creating new state file: %w", err) return fmt.Errorf("creating temp state file: %w", err)
}
tmpFilename := tmp.Name()
defer os.Remove(tmpFilename)
defer tmp.Close()
if _, err := io.Copy(tmp, buf); err != nil {
return fmt.Errorf("writing temp state file: %w", err)
}
if err := tmp.Sync(); err != nil {
return fmt.Errorf("syncing temp state file: %w", err)
} }
if _, err := io.Copy(dst, buf); err != nil { // Closing the temp file before renaming/moving for cross-platform compatibility
return fmt.Errorf("writing to new state file: %w", err) if err := tmp.Close(); err != nil {
return fmt.Errorf("closing temp state file: %w", err)
} }
ext := filepath.Ext(fs.filename)
name, _ := strings.CutSuffix(fs.filename, ext)
backupFilename := name + "." + time.Now().Format("2006-01-02_150405") + ext
if err := os.Rename(fs.filename, backupFilename); err != nil {
if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("rename old state file: %w", err)
}
}
if err := os.Rename(tmpFilename, fs.filename); err != nil {
return fmt.Errorf("atomic rename of state file: %w", err)
}
return nil return nil
} }
@@ -16,7 +16,7 @@ import (
"go.uber.org/mock/gomock" "go.uber.org/mock/gomock"
) )
func TestFileStore_RoundTrip(t *testing.T) { func TestJSONStore_RoundTrip(t *testing.T) {
ctrl := gomock.NewController(t) ctrl := gomock.NewController(t)
ser := roundTripSerializer(ctrl) ser := roundTripSerializer(ctrl)
store, path := newStore(t, "fake", ser) store, path := newStore(t, "fake", ser)
@@ -58,15 +58,52 @@ func TestFileStore_RoundTrip(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("read state file: %v", err) t.Fatalf("read state file: %v", err)
} }
if strings.Contains(string(data), `"reader_data"`) { body := string(data)
if strings.Contains(body, `"reader_data"`) {
t.Errorf("saved state file still contains legacy key \"reader_data\"; want only \"record_data\"") t.Errorf("saved state file still contains legacy key \"reader_data\"; want only \"record_data\"")
} }
if !strings.Contains(string(data), `"record_data"`) { if !strings.Contains(body, `"record_data"`) {
t.Errorf("saved state file does not contain expected key \"record_data\"") t.Errorf("saved state file does not contain expected key \"record_data\"")
} }
// Regression: the "record_data" value must be inline JSON, not a base64
// blob. Version-1 state files encoded the per-record JSON as a base64
// string (which always starts with "ey" — base64 of '{' followed by '"').
// After Chunk 1 of the refactor the value is json.RawMessage, which the
// JSON encoder emits inline. roundTripSerializer emits a JSON string per
// symbol, so the file should contain `"record_data": "AAA"` etc.
if strings.Contains(body, `"record_data": "ey`) {
t.Errorf("saved state file still has base64-encoded record_data (version-1 format); want inline JSON")
}
wantInlineAAA := `"record_data": "AAA"`
if !strings.Contains(body, wantInlineAAA) {
t.Errorf("saved state file does not contain inline JSON %q under record_data; got:\n%s", wantInlineAAA, body)
}
// Sanity: the file must be valid top-level JSON (it always was, but
// confirming the inline form is parseable guards against accidentally
// double-encoding it again in the future).
var parsed struct {
Queues map[string][]struct {
RecordData json.RawMessage `json:"record_data"`
} `json:"queues"`
}
if err := json.Unmarshal(data, &parsed); err != nil {
t.Fatalf("saved state file is not valid JSON: %v", err)
}
for symbol, lots := range parsed.Queues {
for i, lot := range lots {
if len(lot.RecordData) == 0 {
t.Errorf("symbol %q lot %d: record_data is empty", symbol, i)
}
if !json.Valid(lot.RecordData) {
t.Errorf("symbol %q lot %d: record_data is not valid JSON: %s", symbol, i, lot.RecordData)
}
}
}
} }
func TestFileStore_SaveSkipsEmptyQueue(t *testing.T) { func TestJSONStore_SaveSkipsEmptyQueue(t *testing.T) {
ctrl := gomock.NewController(t) ctrl := gomock.NewController(t)
ser := roundTripSerializer(ctrl) ser := roundTripSerializer(ctrl)
store, path := newStore(t, "fake", ser) store, path := newStore(t, "fake", ser)
@@ -96,7 +133,7 @@ func TestFileStore_SaveSkipsEmptyQueue(t *testing.T) {
} }
} }
func TestFileStore_LoadEmptyFileReturnsEmpty(t *testing.T) { func TestJSONStore_LoadEmptyFileReturnsEmpty(t *testing.T) {
ctrl := gomock.NewController(t) ctrl := gomock.NewController(t)
ser := roundTripSerializer(ctrl) ser := roundTripSerializer(ctrl)
store, path := newStore(t, "fake", ser) store, path := newStore(t, "fake", ser)
@@ -119,7 +156,7 @@ func TestFileStore_LoadEmptyFileReturnsEmpty(t *testing.T) {
} }
} }
func TestFileStore_LoadMissingFileReturnsEmpty(t *testing.T) { func TestJSONStore_LoadMissingFileReturnsEmpty(t *testing.T) {
ctrl := gomock.NewController(t) ctrl := gomock.NewController(t)
ser := roundTripSerializer(ctrl) ser := roundTripSerializer(ctrl)
store, path := newStore(t, "fake", ser) store, path := newStore(t, "fake", ser)
@@ -141,7 +178,7 @@ func TestFileStore_LoadMissingFileReturnsEmpty(t *testing.T) {
} }
} }
func TestFileStore_LoadVersionMismatch(t *testing.T) { func TestJSONStore_LoadVersionMismatch(t *testing.T) {
ctrl := gomock.NewController(t) ctrl := gomock.NewController(t)
ser := roundTripSerializer(ctrl) ser := roundTripSerializer(ctrl)
store, path := newStore(t, "fake", ser) store, path := newStore(t, "fake", ser)
@@ -173,7 +210,7 @@ func TestFileStore_LoadVersionMismatch(t *testing.T) {
} }
} }
func TestFileStore_LoadPlatformMismatch(t *testing.T) { func TestJSONStore_LoadPlatformMismatch(t *testing.T) {
ctrl := gomock.NewController(t) ctrl := gomock.NewController(t)
ser := roundTripSerializer(ctrl) ser := roundTripSerializer(ctrl)
store, path := newStore(t, "fake", ser) store, path := newStore(t, "fake", ser)
@@ -205,7 +242,7 @@ func TestFileStore_LoadPlatformMismatch(t *testing.T) {
} }
} }
func TestFileStore_SplitAdjustedLotSurvivesRoundTrip(t *testing.T) { func TestJSONStore_SplitAdjustedLotSurvivesRoundTrip(t *testing.T) {
ctrl := gomock.NewController(t) ctrl := gomock.NewController(t)
ser := roundTripSerializer(ctrl) ser := roundTripSerializer(ctrl)
store, _ := newStore(t, "fake", ser) store, _ := newStore(t, "fake", ser)
@@ -251,7 +288,7 @@ func TestFileStore_SplitAdjustedLotSurvivesRoundTrip(t *testing.T) {
} }
} }
func TestFileStore_PartiallyFilledLotSurvivesRoundTrip(t *testing.T) { func TestJSONStore_PartiallyFilledLotSurvivesRoundTrip(t *testing.T) {
ctrl := gomock.NewController(t) ctrl := gomock.NewController(t)
ser := roundTripSerializer(ctrl) ser := roundTripSerializer(ctrl)
store, _ := newStore(t, "fake", ser) store, _ := newStore(t, "fake", ser)
@@ -292,20 +329,20 @@ func TestFileStore_PartiallyFilledLotSurvivesRoundTrip(t *testing.T) {
} }
} }
func TestNewFileStore_ValidatesArguments(t *testing.T) { func TestNewJSONStore_ValidatesArguments(t *testing.T) {
ctrl := gomock.NewController(t) ctrl := gomock.NewController(t)
ser := mocks.NewMockRecordSerializer(ctrl) ser := mocks.NewMockRecordSerializer(ctrl)
if _, err := internal.NewFileStore("", "fake", ser); err == nil { if _, err := internal.NewJSONStore("", "fake", ser); err == nil {
t.Errorf("NewFileStore with empty filename should fail") t.Errorf("NewJSONStore with empty filename should fail")
} }
if _, err := internal.NewFileStore("/tmp/x", "fake", nil); err == nil { if _, err := internal.NewJSONStore("/tmp/x", "fake", nil); err == nil {
t.Errorf("NewFileStore with nil serializer should fail") t.Errorf("NewJSONStore with nil serializer should fail")
} }
} }
// newRecord builds a MockRecord whose Symbol() returns the given symbol. // newRecord builds a MockRecord whose Symbol() returns the given symbol.
// The FileStore only reads Symbol() off the loaded Record during tests // The JSONStore only reads Symbol() off the loaded Record during tests
// (quantity/price/filled come from the persisted struct fields), so all // (quantity/price/filled come from the persisted struct fields), so all
// other Record methods can be left as default-mocked values. // other Record methods can be left as default-mocked values.
func newRecord(ctrl *gomock.Controller, symbol string) *mocks.MockRecord { func newRecord(ctrl *gomock.Controller, symbol string) *mocks.MockRecord {
@@ -335,35 +372,39 @@ func newQueue(fillers ...*internal.Filler) *internal.FillerQueue {
return q return q
} }
// newStore creates a FileStore backed by a temp file and returns the store // newStore creates a JSONStore backed by a temp file and returns the store
// plus the path to the underlying state file. // plus the path to the underlying state file.
func newStore(t *testing.T, platform string, ser internal.RecordSerializer) (*internal.FileStore, string) { func newStore(t *testing.T, platform string, ser internal.RecordSerializer) (*internal.JSONStore, string) {
t.Helper() t.Helper()
dir := t.TempDir() dir := t.TempDir()
path := filepath.Join(dir, "state.json") path := filepath.Join(dir, "state.json")
store, err := internal.NewFileStore(path, platform, ser) store, err := internal.NewJSONStore(path, platform, ser)
if err != nil { if err != nil {
t.Fatalf("NewFileStore returned unexpected error: %v", err) t.Fatalf("NewJSONStore returned unexpected error: %v", err)
} }
return store, path return store, path
} }
// roundTripSerializer returns a serializer mock whose MarshalRecord encodes // roundTripSerializer returns a serializer mock whose MarshalRecord encodes
// the Symbol into bytes and whose UnmarshalRecord decodes those bytes back // the Symbol as a JSON string (e.g. "AAA") and whose UnmarshalRecord decodes
// into a fresh MockRecord returning the encoded Symbol. Tests use this when // those JSON bytes back into a fresh MockRecord returning the encoded Symbol.
// they need Save + Load to round-trip equivalent Records. // Tests use this when they need Save + Load to round-trip equivalent Records.
func roundTripSerializer(ctrl *gomock.Controller) *mocks.MockRecordSerializer { func roundTripSerializer(ctrl *gomock.Controller) *mocks.MockRecordSerializer {
ser := mocks.NewMockRecordSerializer(ctrl) ser := mocks.NewMockRecordSerializer(ctrl)
ser.EXPECT(). ser.EXPECT().
MarshalRecord(gomock.Any(), gomock.Any()). MarshalRecord(gomock.Any(), gomock.Any()).
DoAndReturn(func(_ context.Context, r internal.Record) ([]byte, error) { DoAndReturn(func(_ context.Context, r internal.Record) (json.RawMessage, error) {
return []byte(r.Symbol()), nil return json.Marshal(r.Symbol())
}). }).
AnyTimes() AnyTimes()
ser.EXPECT(). ser.EXPECT().
UnmarshalRecord(gomock.Any(), gomock.Any()). UnmarshalRecord(gomock.Any(), gomock.Any()).
DoAndReturn(func(_ context.Context, b []byte) (internal.Record, error) { DoAndReturn(func(_ context.Context, b json.RawMessage) (internal.Record, error) {
return newRecord(ctrl, string(b)), nil var symbol string
if err := json.Unmarshal(b, &symbol); err != nil {
return nil, err
}
return newRecord(ctrl, symbol), nil
}). }).
AnyTimes() AnyTimes()
return ser return ser
+17 -16
View File
@@ -11,6 +11,7 @@ package mocks
import ( import (
context "context" context "context"
json "encoding/json"
reflect "reflect" reflect "reflect"
time "time" time "time"
@@ -573,10 +574,10 @@ func (m *MockRecordEncoder) EXPECT() *MockRecordEncoderMockRecorder {
} }
// MarshalRecord mocks base method. // MarshalRecord mocks base method.
func (m *MockRecordEncoder) MarshalRecord(arg0 context.Context, arg1 internal.Record) ([]byte, error) { func (m *MockRecordEncoder) MarshalRecord(arg0 context.Context, arg1 internal.Record) (json.RawMessage, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MarshalRecord", arg0, arg1) ret := m.ctrl.Call(m, "MarshalRecord", arg0, arg1)
ret0, _ := ret[0].([]byte) ret0, _ := ret[0].(json.RawMessage)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
@@ -594,19 +595,19 @@ type MockRecordEncoderMarshalRecordCall struct {
} }
// Return rewrite *gomock.Call.Return // Return rewrite *gomock.Call.Return
func (c *MockRecordEncoderMarshalRecordCall) Return(arg0 []byte, arg1 error) *MockRecordEncoderMarshalRecordCall { func (c *MockRecordEncoderMarshalRecordCall) Return(arg0 json.RawMessage, arg1 error) *MockRecordEncoderMarshalRecordCall {
c.Call = c.Call.Return(arg0, arg1) c.Call = c.Call.Return(arg0, arg1)
return c return c
} }
// Do rewrite *gomock.Call.Do // Do rewrite *gomock.Call.Do
func (c *MockRecordEncoderMarshalRecordCall) Do(f func(context.Context, internal.Record) ([]byte, error)) *MockRecordEncoderMarshalRecordCall { func (c *MockRecordEncoderMarshalRecordCall) Do(f func(context.Context, internal.Record) (json.RawMessage, error)) *MockRecordEncoderMarshalRecordCall {
c.Call = c.Call.Do(f) c.Call = c.Call.Do(f)
return c return c
} }
// DoAndReturn rewrite *gomock.Call.DoAndReturn // DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockRecordEncoderMarshalRecordCall) DoAndReturn(f func(context.Context, internal.Record) ([]byte, error)) *MockRecordEncoderMarshalRecordCall { func (c *MockRecordEncoderMarshalRecordCall) DoAndReturn(f func(context.Context, internal.Record) (json.RawMessage, error)) *MockRecordEncoderMarshalRecordCall {
c.Call = c.Call.DoAndReturn(f) c.Call = c.Call.DoAndReturn(f)
return c return c
} }
@@ -636,7 +637,7 @@ func (m *MockRecordDecoder) EXPECT() *MockRecordDecoderMockRecorder {
} }
// UnmarshalRecord mocks base method. // UnmarshalRecord mocks base method.
func (m *MockRecordDecoder) UnmarshalRecord(arg0 context.Context, arg1 []byte) (internal.Record, error) { func (m *MockRecordDecoder) UnmarshalRecord(arg0 context.Context, arg1 json.RawMessage) (internal.Record, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UnmarshalRecord", arg0, arg1) ret := m.ctrl.Call(m, "UnmarshalRecord", arg0, arg1)
ret0, _ := ret[0].(internal.Record) ret0, _ := ret[0].(internal.Record)
@@ -663,13 +664,13 @@ func (c *MockRecordDecoderUnmarshalRecordCall) Return(arg0 internal.Record, arg1
} }
// Do rewrite *gomock.Call.Do // Do rewrite *gomock.Call.Do
func (c *MockRecordDecoderUnmarshalRecordCall) Do(f func(context.Context, []byte) (internal.Record, error)) *MockRecordDecoderUnmarshalRecordCall { func (c *MockRecordDecoderUnmarshalRecordCall) Do(f func(context.Context, json.RawMessage) (internal.Record, error)) *MockRecordDecoderUnmarshalRecordCall {
c.Call = c.Call.Do(f) c.Call = c.Call.Do(f)
return c return c
} }
// DoAndReturn rewrite *gomock.Call.DoAndReturn // DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockRecordDecoderUnmarshalRecordCall) DoAndReturn(f func(context.Context, []byte) (internal.Record, error)) *MockRecordDecoderUnmarshalRecordCall { func (c *MockRecordDecoderUnmarshalRecordCall) DoAndReturn(f func(context.Context, json.RawMessage) (internal.Record, error)) *MockRecordDecoderUnmarshalRecordCall {
c.Call = c.Call.DoAndReturn(f) c.Call = c.Call.DoAndReturn(f)
return c return c
} }
@@ -699,10 +700,10 @@ func (m *MockRecordSerializer) EXPECT() *MockRecordSerializerMockRecorder {
} }
// MarshalRecord mocks base method. // MarshalRecord mocks base method.
func (m *MockRecordSerializer) MarshalRecord(arg0 context.Context, arg1 internal.Record) ([]byte, error) { func (m *MockRecordSerializer) MarshalRecord(arg0 context.Context, arg1 internal.Record) (json.RawMessage, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MarshalRecord", arg0, arg1) ret := m.ctrl.Call(m, "MarshalRecord", arg0, arg1)
ret0, _ := ret[0].([]byte) ret0, _ := ret[0].(json.RawMessage)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
@@ -720,25 +721,25 @@ type MockRecordSerializerMarshalRecordCall struct {
} }
// Return rewrite *gomock.Call.Return // Return rewrite *gomock.Call.Return
func (c *MockRecordSerializerMarshalRecordCall) Return(arg0 []byte, arg1 error) *MockRecordSerializerMarshalRecordCall { func (c *MockRecordSerializerMarshalRecordCall) Return(arg0 json.RawMessage, arg1 error) *MockRecordSerializerMarshalRecordCall {
c.Call = c.Call.Return(arg0, arg1) c.Call = c.Call.Return(arg0, arg1)
return c return c
} }
// Do rewrite *gomock.Call.Do // Do rewrite *gomock.Call.Do
func (c *MockRecordSerializerMarshalRecordCall) Do(f func(context.Context, internal.Record) ([]byte, error)) *MockRecordSerializerMarshalRecordCall { func (c *MockRecordSerializerMarshalRecordCall) Do(f func(context.Context, internal.Record) (json.RawMessage, error)) *MockRecordSerializerMarshalRecordCall {
c.Call = c.Call.Do(f) c.Call = c.Call.Do(f)
return c return c
} }
// DoAndReturn rewrite *gomock.Call.DoAndReturn // DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockRecordSerializerMarshalRecordCall) DoAndReturn(f func(context.Context, internal.Record) ([]byte, error)) *MockRecordSerializerMarshalRecordCall { func (c *MockRecordSerializerMarshalRecordCall) DoAndReturn(f func(context.Context, internal.Record) (json.RawMessage, error)) *MockRecordSerializerMarshalRecordCall {
c.Call = c.Call.DoAndReturn(f) c.Call = c.Call.DoAndReturn(f)
return c return c
} }
// UnmarshalRecord mocks base method. // UnmarshalRecord mocks base method.
func (m *MockRecordSerializer) UnmarshalRecord(arg0 context.Context, arg1 []byte) (internal.Record, error) { func (m *MockRecordSerializer) UnmarshalRecord(arg0 context.Context, arg1 json.RawMessage) (internal.Record, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UnmarshalRecord", arg0, arg1) ret := m.ctrl.Call(m, "UnmarshalRecord", arg0, arg1)
ret0, _ := ret[0].(internal.Record) ret0, _ := ret[0].(internal.Record)
@@ -765,13 +766,13 @@ func (c *MockRecordSerializerUnmarshalRecordCall) Return(arg0 internal.Record, a
} }
// Do rewrite *gomock.Call.Do // Do rewrite *gomock.Call.Do
func (c *MockRecordSerializerUnmarshalRecordCall) Do(f func(context.Context, []byte) (internal.Record, error)) *MockRecordSerializerUnmarshalRecordCall { func (c *MockRecordSerializerUnmarshalRecordCall) Do(f func(context.Context, json.RawMessage) (internal.Record, error)) *MockRecordSerializerUnmarshalRecordCall {
c.Call = c.Call.Do(f) c.Call = c.Call.Do(f)
return c return c
} }
// DoAndReturn rewrite *gomock.Call.DoAndReturn // DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockRecordSerializerUnmarshalRecordCall) DoAndReturn(f func(context.Context, []byte) (internal.Record, error)) *MockRecordSerializerUnmarshalRecordCall { func (c *MockRecordSerializerUnmarshalRecordCall) DoAndReturn(f func(context.Context, json.RawMessage) (internal.Record, error)) *MockRecordSerializerUnmarshalRecordCall {
c.Call = c.Call.DoAndReturn(f) c.Call = c.Call.DoAndReturn(f)
return c return c
} }
+7 -6
View File
@@ -2,12 +2,13 @@ package internal
import ( import (
"context" "context"
"encoding/json"
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
) )
// StateVersion is the schema version of the persisted State struct. // StateVersion is the schema version of the persisted State struct.
const StateVersion = "1" const StateVersion = "2"
// State is the platform-agnostic representation of the buy-queue state that is // State is the platform-agnostic representation of the buy-queue state that is
// written to disk after a successful run and reloaded on the next run. // written to disk after a successful run and reloaded on the next run.
@@ -21,20 +22,20 @@ type State struct {
// Record-specific data is kept as opaque bytes so the internal package does // Record-specific data is kept as opaque bytes so the internal package does
// not need to know about any broker package's concrete Record type. // not need to know about any broker package's concrete Record type.
type persistedFiller struct { type persistedFiller struct {
RecordData []byte `json:"record_data"` RecordData json.RawMessage `json:"record_data"`
Quantity decimal.Decimal `json:"quantity"` Quantity decimal.Decimal `json:"quantity"`
Price decimal.Decimal `json:"price"` Price decimal.Decimal `json:"price"`
Filled decimal.Decimal `json:"filled"` Filled decimal.Decimal `json:"filled"`
} }
// RecordEncoder encodes a Record into its broker-specific byte representation. // RecordEncoder encodes a Record into its broker-specific JSON representation.
type RecordEncoder interface { type RecordEncoder interface {
MarshalRecord(context.Context, Record) ([]byte, error) MarshalRecord(context.Context, Record) (json.RawMessage, error)
} }
// RecordDecoder decodes broker-specific bytes back into a Record. // RecordDecoder decodes broker-specific JSON back into a Record.
type RecordDecoder interface { type RecordDecoder interface {
UnmarshalRecord(context.Context, []byte) (Record, error) UnmarshalRecord(context.Context, json.RawMessage) (Record, error)
} }
// RecordSerializer composes encoding and decoding of Records. Broker packages // RecordSerializer composes encoding and decoding of Records. Broker packages
+2 -2
View File
@@ -40,7 +40,7 @@ func NewRecordSerializer(figi *ofigi.Client) *RecordSerializer {
// MarshalRecord encodes the given internal.Record as JSON. It returns an // MarshalRecord encodes the given internal.Record as JSON. It returns an
// error if the record is not a trading212.Record (or a *trading212.Record). // error if the record is not a trading212.Record (or a *trading212.Record).
func (s *RecordSerializer) MarshalRecord(_ context.Context, r internal.Record) ([]byte, error) { func (s *RecordSerializer) MarshalRecord(_ context.Context, r internal.Record) (json.RawMessage, error) {
var rec Record var rec Record
switch v := r.(type) { switch v := r.(type) {
case Record: case Record:
@@ -69,7 +69,7 @@ func (s *RecordSerializer) MarshalRecord(_ context.Context, r internal.Record) (
// UnmarshalRecord decodes a JSON-encoded trading212.Record and re-wires its // UnmarshalRecord decodes a JSON-encoded trading212.Record and re-wires its
// natureGetter to the serializer's OpenFIGI client. // natureGetter to the serializer's OpenFIGI client.
func (s *RecordSerializer) UnmarshalRecord(ctx context.Context, data []byte) (internal.Record, error) { func (s *RecordSerializer) UnmarshalRecord(ctx context.Context, data json.RawMessage) (internal.Record, error) {
var state recordState var state recordState
if err := json.Unmarshal(data, &state); err != nil { if err := json.Unmarshal(data, &state); err != nil {
return nil, fmt.Errorf("unmarshal trading212 record: %w", err) return nil, fmt.Errorf("unmarshal trading212 record: %w", err)