mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-06-06 15:10:59 +00:00
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
This commit is contained in:
150
exchanges/subscription/template.go
Normal file
150
exchanges/subscription/template.go
Normal file
@@ -0,0 +1,150 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user