diff options
Diffstat (limited to 'mt4')
-rw-r--r-- | mt4/client.go | 115 | ||||
-rw-r--r-- | mt4/client_test.go | 45 |
2 files changed, 160 insertions, 0 deletions
diff --git a/mt4/client.go b/mt4/client.go new file mode 100644 index 0000000..8a5f388 --- /dev/null +++ b/mt4/client.go @@ -0,0 +1,115 @@ +package mt4 + +import ( + "context" + "fmt" + "net" + "time" + + "go.popov.link/metatrader4/internal/conn" + "go.popov.link/metatrader4/internal/proto" +) + +// Client provides access to a MetaTrader4 server. +type Client struct { + addr string + port int + dialTimeout time.Duration + readTimeout time.Duration + writeTimeout time.Duration + autoClose bool + dialer net.Dialer + c *conn.Conn +} + +// Option configures the Client. +type Option func(*Client) + +// WithDialTimeout sets timeout for establishing connections. +func WithDialTimeout(d time.Duration) Option { return func(c *Client) { c.dialTimeout = d } } + +// WithReadTimeout sets timeout for reading responses. +func WithReadTimeout(d time.Duration) Option { return func(c *Client) { c.readTimeout = d } } + +// WithWriteTimeout sets timeout for writing requests. +func WithWriteTimeout(d time.Duration) Option { return func(c *Client) { c.writeTimeout = d } } + +// WithAutoClose enables or disables automatic connection close after Execute. +func WithAutoClose(b bool) Option { return func(c *Client) { c.autoClose = b } } + +// NewClient creates a new Client with optional configuration. +func NewClient(addr string, port int, opts ...Option) *Client { + cl := &Client{ + addr: addr, + port: port, + dialTimeout: 5 * time.Second, + readTimeout: 5 * time.Second, + writeTimeout: 5 * time.Second, + autoClose: true, + } + for _, o := range opts { + o(cl) + } + return cl +} + +// Connect establishes connection to the MT4 server if not already connected. +func (c *Client) Connect(ctx context.Context) error { + if c.c != nil { + return nil + } + address := fmt.Sprintf("%s:%d", c.addr, c.port) + cn, err := conn.Dial(ctx, address, c.dialTimeout) + if err != nil { + return err + } + c.c = cn + return nil +} + +// Close closes underlying connection. +func (c *Client) Close() error { + if c.c == nil { + return nil + } + err := c.c.Close() + c.c = nil + return err +} + +// Execute sends command with params to the server and returns decoded response. +func (c *Client) Execute(ctx context.Context, command string, params map[string]string) (string, error) { + if err := c.Connect(ctx); err != nil { + return "", fmt.Errorf("connect: %w", err) + } + + encoded, err := proto.EncodeParams(params) + if err != nil { + if c.autoClose { + c.Close() + } + return "", err + } + req := proto.BuildRequest(command, encoded, c.autoClose) + + if err := c.c.Send(ctx, req, c.writeTimeout); err != nil { + if c.autoClose { + c.Close() + } + return "", fmt.Errorf("send: %w", err) + } + + respBytes, err := c.c.Receive(ctx, c.readTimeout) + if c.autoClose { + c.Close() + } + if err != nil { + return "", fmt.Errorf("receive: %w", err) + } + + resp, err := proto.DecodeResponse(string(respBytes)) + if err != nil { + return "", err + } + return resp, nil +} diff --git a/mt4/client_test.go b/mt4/client_test.go new file mode 100644 index 0000000..9644c9a --- /dev/null +++ b/mt4/client_test.go @@ -0,0 +1,45 @@ +package mt4 + +import ( + "context" + "net" + "strings" + "testing" + "time" + + ic "go.popov.link/metatrader4/internal/conn" + "go.popov.link/metatrader4/internal/proto" +) + +// mockServer returns net.Pipe connections with server writing resp to client. +func mockServer(response string) (net.Conn, net.Conn) { + server, client := net.Pipe() + go func() { + defer server.Close() + buf := make([]byte, 1024) + server.Read(buf) // read request ignoring + server.Write([]byte(response)) + }() + return client, server +} + +func TestClientExecute(t *testing.T) { + reqParams := map[string]string{"A": "1"} + encoded, err := proto.EncodeParams(reqParams) + if err != nil { + t.Fatalf("encode params: %v", err) + } + resp := encoded + clientConn, _ := mockServer(resp) + + c := &Client{addr: "", port: 0, autoClose: true, readTimeout: time.Second, writeTimeout: time.Second, dialTimeout: time.Second} + c.c = ic.FromNetConn(clientConn) + + res, err := c.Execute(context.Background(), "CMD", reqParams) + if err != nil { + t.Fatalf("execute: %v", err) + } + if !strings.Contains(res, "1") { + t.Fatalf("unexpected response %q", res) + } +} |