Reviewed-on: #30 Co-authored-by: Natercio Moniz <[email protected]>
This commit was merged in pull request #30.
This commit is contained in:
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user