@@ -0,0 +1,163 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user