164 lines
4.8 KiB
Go
164 lines
4.8 KiB
Go
package vipcardexperimentmod
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
|
|
"go.mongodb.org/mongo-driver/bson"
|
|
"go.mongodb.org/mongo-driver/bson/bsontype"
|
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
|
)
|
|
|
|
// OrderedProductIDs exposes productIds as an order-number-to-product-ID map
|
|
// while keeping a slice internally so every backend consumer has deterministic
|
|
// numeric order. BSON is stored as an embedded document with keys 1..n.
|
|
// Legacy JSON/BSON arrays remain readable for existing experiment records.
|
|
type OrderedProductIDs []primitive.ObjectID
|
|
|
|
func (ids OrderedProductIDs) MarshalJSON() ([]byte, error) {
|
|
var buffer bytes.Buffer
|
|
buffer.WriteByte('{')
|
|
for index, productID := range ids {
|
|
if index > 0 {
|
|
buffer.WriteByte(',')
|
|
}
|
|
key, _ := json.Marshal(strconv.Itoa(index + 1))
|
|
value, err := json.Marshal(productID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
buffer.Write(key)
|
|
buffer.WriteByte(':')
|
|
buffer.Write(value)
|
|
}
|
|
buffer.WriteByte('}')
|
|
return buffer.Bytes(), nil
|
|
}
|
|
|
|
func (ids *OrderedProductIDs) UnmarshalJSON(data []byte) error {
|
|
data = bytes.TrimSpace(data)
|
|
if len(data) == 0 {
|
|
return fmt.Errorf("productIds is required")
|
|
}
|
|
if bytes.Equal(data, []byte("null")) {
|
|
*ids = nil
|
|
return nil
|
|
}
|
|
switch data[0] {
|
|
case '[':
|
|
var legacy []primitive.ObjectID
|
|
if err := json.Unmarshal(data, &legacy); err != nil {
|
|
return fmt.Errorf("invalid legacy productIds: %w", err)
|
|
}
|
|
*ids = legacy
|
|
return nil
|
|
case '{':
|
|
var values map[string]json.RawMessage
|
|
if err := json.Unmarshal(data, &values); err != nil {
|
|
return fmt.Errorf("invalid productIds map: %w", err)
|
|
}
|
|
ordered := make(OrderedProductIDs, len(values))
|
|
occupied := make([]bool, len(values))
|
|
seenProducts := make(map[primitive.ObjectID]struct{}, len(values))
|
|
for key, rawProductID := range values {
|
|
index, err := parseProductOrder(key, len(values))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var productID primitive.ObjectID
|
|
if err = json.Unmarshal(rawProductID, &productID); err != nil || productID.IsZero() {
|
|
return fmt.Errorf("productIds.%s must be a valid product ID", key)
|
|
}
|
|
if occupied[index] {
|
|
return fmt.Errorf("productIds order %s is duplicated", key)
|
|
}
|
|
if _, exists := seenProducts[productID]; exists {
|
|
return fmt.Errorf("productIds contains duplicate product ID: %s", productID.Hex())
|
|
}
|
|
occupied[index] = true
|
|
seenProducts[productID] = struct{}{}
|
|
ordered[index] = productID
|
|
}
|
|
*ids = ordered
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("productIds must be an order map")
|
|
}
|
|
}
|
|
|
|
func (ids OrderedProductIDs) MarshalBSONValue() (bsontype.Type, []byte, error) {
|
|
document := make(bson.D, 0, len(ids))
|
|
for index, productID := range ids {
|
|
document = append(document, bson.E{
|
|
Key: strconv.Itoa(index + 1),
|
|
Value: productID,
|
|
})
|
|
}
|
|
return bson.MarshalValue(document)
|
|
}
|
|
|
|
func (ids *OrderedProductIDs) UnmarshalBSONValue(valueType bsontype.Type, data []byte) error {
|
|
rawValue := bson.RawValue{Type: valueType, Value: data}
|
|
switch valueType {
|
|
case bsontype.Array:
|
|
var legacy []primitive.ObjectID
|
|
if err := rawValue.Unmarshal(&legacy); err != nil {
|
|
return fmt.Errorf("invalid legacy productIds: %w", err)
|
|
}
|
|
*ids = legacy
|
|
return nil
|
|
case bsontype.EmbeddedDocument:
|
|
var document bson.Raw
|
|
if err := rawValue.Unmarshal(&document); err != nil {
|
|
return fmt.Errorf("invalid productIds document: %w", err)
|
|
}
|
|
elements, err := document.Elements()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid productIds document: %w", err)
|
|
}
|
|
ordered := make(OrderedProductIDs, len(elements))
|
|
occupied := make([]bool, len(elements))
|
|
seenProducts := make(map[primitive.ObjectID]struct{}, len(elements))
|
|
for _, element := range elements {
|
|
key := element.Key()
|
|
index, parseErr := parseProductOrder(key, len(elements))
|
|
if parseErr != nil {
|
|
return parseErr
|
|
}
|
|
productID, ok := element.Value().ObjectIDOK()
|
|
if !ok || productID.IsZero() {
|
|
return fmt.Errorf("productIds.%s must be a valid product ID", key)
|
|
}
|
|
if occupied[index] {
|
|
return fmt.Errorf("productIds order %s is duplicated", key)
|
|
}
|
|
if _, exists := seenProducts[productID]; exists {
|
|
return fmt.Errorf("productIds contains duplicate product ID: %s", productID.Hex())
|
|
}
|
|
occupied[index] = true
|
|
seenProducts[productID] = struct{}{}
|
|
ordered[index] = productID
|
|
}
|
|
*ids = ordered
|
|
return nil
|
|
case bsontype.Null, bsontype.Undefined:
|
|
*ids = nil
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("productIds must be an ordered document")
|
|
}
|
|
}
|
|
|
|
func parseProductOrder(key string, size int) (int, error) {
|
|
order, err := strconv.Atoi(key)
|
|
if err != nil || order <= 0 || strconv.Itoa(order) != key {
|
|
return 0, fmt.Errorf("productIds order must be a positive integer: %s", key)
|
|
}
|
|
if order > size {
|
|
return 0, fmt.Errorf("productIds order must be continuous from 1")
|
|
}
|
|
return order - 1, nil
|
|
}
|