Files
gocryptotrader/exchanges/subscription/template.go
Gareth Kirwan c601575c66 subscriptions: Add templating support and integrate with Binance (#1568)
* Subscriptions: Add List.AssetPairs

* Subscriptions: Add Template and QualifiedChannel

These fields separate the concept of what the channel is from the
qualified resource name

* Subscriptions: Add List.SetStates()

* Subscriptions: Add List.QualifiedChannels

* Subscriptions: Rename testsubs.EqualLists

* Binance: Switch to ExpandTemplates

* Binance: Update ConfigTest format

* Subscriptions: Test Coverage improvements

* Subscriptions: Reenterant List.ExpandTemplates

* Subscriptions: Move templates from subscriptions to exchanges

* Binance: Inline subscription template and improvements
2024-07-09 15:53:00 +10:00

151 lines
4.3 KiB
Go

package subscription
import (
"bytes"
"errors"
"fmt"
"slices"
"strings"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
const (
groupSeparator = "\x1D"
recordSeparator = "\x1E"
)
var (
errInvalidAssetExpandPairs = errors.New("subscription template containing PairSeparator with must contain either specific Asset or AssetSeparator")
errAssetRecords = errors.New("subscription template did not generate the expected number of asset records")
errPairRecords = errors.New("subscription template did not generate the expected number of pair records")
errAssetTemplateWithoutAll = errors.New("sub.Asset must be set to All if AssetSeparator is used in Channel template")
errNoTemplateContent = errors.New("subscription template did not generate content")
errInvalidTemplate = errors.New("GetSubscriptionTemplate did not return a template")
)
type tplCtx struct {
S *Subscription
AssetPairs assetPairs
PairSeparator string
AssetSeparator string
}
// ExpandTemplates returns a list of Subscriptions with Template expanded
// May be called on already expanded subscriptions: Passes $s through unprocessed if QualifiedChannel is already populated
// Calls e.GetSubscriptionTemplate to find a template for each subscription
// Filters out Authenticated subscriptions if !e.CanUseAuthenticatedEndpoints
// See README.md for more details
func (l List) ExpandTemplates(e iExchange) (List, error) {
if !slices.ContainsFunc(l, func(s *Subscription) bool { return s.QualifiedChannel == "" }) {
// Empty list, or already processed
return slices.Clone(l), nil
}
if !e.CanUseAuthenticatedWebsocketEndpoints() {
n := List{}
for _, s := range l {
if !s.Authenticated {
n = append(n, s)
}
}
l = n
}
ap, err := l.assetPairs(e)
if err != nil {
return nil, err
}
assets := make(asset.Items, 0, len(ap))
for k := range ap {
assets = append(assets, k)
}
slices.Sort(assets) // text/template ranges maps in sorted order
subs := List{}
for _, s := range l {
if s.QualifiedChannel != "" {
subs = append(subs, s)
continue
}
subCtx := &tplCtx{
S: s,
AssetPairs: ap,
PairSeparator: recordSeparator,
AssetSeparator: groupSeparator,
}
t, err := e.GetSubscriptionTemplate(s)
if err != nil {
return nil, err
}
if t == nil {
return nil, errInvalidTemplate
}
buf := &bytes.Buffer{}
if err := t.Execute(buf, subCtx); err != nil {
return nil, err
}
out := buf.String()
subAssets := assets
xpandPairs := strings.Contains(out, subCtx.PairSeparator)
if xpandAssets := strings.Contains(out, subCtx.AssetSeparator); xpandAssets {
if s.Asset != asset.All {
return nil, errAssetTemplateWithoutAll
}
} else {
if xpandPairs && (s.Asset == asset.All || s.Asset == asset.Empty) {
// We don't currently support expanding Pairs without expanding Assets for All or Empty assets, but we could; waiting for a use-case
return nil, errInvalidAssetExpandPairs
}
// No expansion so update expected Assets for consistent behaviour below
subAssets = []asset.Item{s.Asset}
}
out = strings.TrimRight(out, " \n\r\t"+subCtx.PairSeparator+subCtx.AssetSeparator)
assetRecords := strings.Split(out, subCtx.AssetSeparator)
if len(assetRecords) != len(subAssets) {
return nil, fmt.Errorf("%w: Got %d; Expected %d", errAssetRecords, len(assetRecords), len(subAssets))
}
for i, assetChannels := range assetRecords {
a := subAssets[i]
assetChannels = strings.TrimRight(assetChannels, " \n\r\t"+recordSeparator)
pairLines := strings.Split(assetChannels, subCtx.PairSeparator)
pairs, ok := ap[a]
if xpandPairs {
if !ok {
return nil, fmt.Errorf("%w: %s", asset.ErrInvalidAsset, a)
}
if len(pairLines) != len(pairs) {
return nil, fmt.Errorf("%w: Got %d; Expected %d", errPairRecords, len(pairLines), len(pairs))
}
}
for j, channel := range pairLines {
c := s.Clone()
c.Asset = a
channel = strings.TrimSpace(channel)
if channel == "" {
return nil, fmt.Errorf("%w: %s", errNoTemplateContent, s)
}
c.QualifiedChannel = strings.TrimSpace(channel)
if xpandPairs {
c.Pairs = currency.Pairs{pairs[j]}
} else {
c.Pairs = pairs
}
subs = append(subs, c)
}
}
}
return subs, nil
}