openmeter / subscription /uniqueness.go
Leon4gr45's picture
Upload folder using huggingface_hub (part 7)
16cdcb7 verified
Raw
History Blame Contribute Delete
9.74 kB
package subscription
import (
"errors"
"fmt"
"github.com/samber/lo"
"github.com/openmeterio/openmeter/pkg/models"
"github.com/openmeterio/openmeter/pkg/slicesx"
)
func ValidateUniqueConstraintBySubscriptions(subs []SubscriptionSpec) error {
var errs []error
if overlaps := models.NewSortedCadenceList(subs).GetOverlaps(); len(overlaps) > 0 {
for _, overlap := range overlaps {
// to get proper selectors we'll add two errors (one for each side)
errs = append(errs,
ErrOnlySingleSubscriptionAllowed.WithAttrs(models.Attributes{
ErrCodeOnlySingleSubscriptionAllowed: SubscriptionSubscriptionLevelUniqueConstraintErrorDetail{
This: SubscriptionSubscriptionLevelUniqueConstraintErrorDetailSide{
Subscription: overlap.Item1,
Cadence: overlap.Item1.GetCadence(),
Selectors: subscriptionSpecToFieldSelectors(overlap.Item1),
},
Other: SubscriptionSubscriptionLevelUniqueConstraintErrorDetailSide{
Subscription: overlap.Item2,
Cadence: overlap.Item2.GetCadence(),
Selectors: subscriptionSpecToFieldSelectors(overlap.Item2),
},
},
}).WithField(subscriptionSpecToFieldSelectors(overlap.Item1)))
errs = append(errs,
ErrOnlySingleSubscriptionAllowed.WithAttrs(models.Attributes{
ErrCodeOnlySingleSubscriptionAllowed: SubscriptionSubscriptionLevelUniqueConstraintErrorDetail{
This: SubscriptionSubscriptionLevelUniqueConstraintErrorDetailSide{
Subscription: overlap.Item2,
Cadence: overlap.Item2.GetCadence(),
Selectors: subscriptionSpecToFieldSelectors(overlap.Item2),
},
Other: SubscriptionSubscriptionLevelUniqueConstraintErrorDetailSide{
Subscription: overlap.Item1,
Cadence: overlap.Item1.GetCadence(),
Selectors: subscriptionSpecToFieldSelectors(overlap.Item1),
},
},
}).WithField(subscriptionSpecToFieldSelectors(overlap.Item2)))
}
}
return errors.Join(errs...)
}
func ValidateUniqueConstraintByFeatures(subs []SubscriptionSpec) error {
return featureLevelUniqueConstraintValidator{}.Validate(subs)
}
type SubscriptionSubscriptionLevelUniqueConstraintErrorDetailSide struct {
Subscription SubscriptionSpec `json:"subscription"`
Cadence models.CadencedModel `json:"cadence"`
Selectors *models.FieldDescriptor `json:"selectors"`
}
type SubscriptionSubscriptionLevelUniqueConstraintErrorDetail = models.Overlap[SubscriptionSubscriptionLevelUniqueConstraintErrorDetailSide]
type SubscriptionFeatureLevelUniqueConstraintErrorDetailSide struct {
Item SubscriptionItemSpec `json:"-"` // useful internally but let's not expose it to the client
Cadence models.CadencedModel `json:"cadence"`
Selectors *models.FieldDescriptor `json:"selectors"`
PlanRef PlanRef `json:"plan_ref"`
}
type SubscriptionFeatureLevelUniqueConstraintErrorDetail = models.Overlap[SubscriptionFeatureLevelUniqueConstraintErrorDetailSide]
// let's localize all logic on this struct to avoid scope pollution
type featureLevelUniqueConstraintValidator struct{}
func (v featureLevelUniqueConstraintValidator) Validate(subs []SubscriptionSpec) error {
relevantItems := v.collectRelevantItems(subs, v.itemIsRelevant)
timelinesForRelevantItems, err := v.buildRelevantTimelines(relevantItems)
if err != nil {
return err
}
var errs []error
for _, timeline := range timelinesForRelevantItems {
if overlaps := timeline.GetOverlaps(); len(overlaps) > 0 {
for _, overlap := range overlaps {
// To get proper FieldSelectors, we'll add two errors (one for each side)
errs = append(errs,
ErrOnlySingleSubscriptionItemAllowedAtATime.
WithAttrs(overlap.Item1.GetErrorAttributes(overlap.Item2)).
WithField(overlap.Item1.Item.GetSelectors()))
errs = append(errs,
ErrOnlySingleSubscriptionItemAllowedAtATime.
WithAttrs(overlap.Item2.GetErrorAttributes(overlap.Item1)).
WithField(overlap.Item2.Item.GetSelectors()))
}
}
}
return errors.Join(errs...)
}
func (v featureLevelUniqueConstraintValidator) buildRelevantTimelines(itemMap map[string][]itemSpecWithCircularReferences) (map[string]models.CadenceList[validationTimelineEntry], error) {
timelines := make(map[string]models.CadenceList[validationTimelineEntry])
for itemKey, items := range itemMap {
validationTimelineEntries, err := slicesx.MapWithErr(items, func(item itemSpecWithCircularReferences) (validationTimelineEntry, error) {
phaseCadence, err := item.SubscriptionSpec.GetPhaseCadence(item.SubscriptionPhaseSpec.PhaseKey)
if err != nil {
return validationTimelineEntry{}, fmt.Errorf("failed to get phase cadence for item %s: %w", itemKey, err)
}
itemCadence := item.SubscriptionItemSpec.GetCadence(phaseCadence)
return validationTimelineEntry{
Item: &item,
Cadence: itemCadence,
}, nil
})
if err != nil {
return nil, err
}
timelines[itemKey] = models.NewSortedCadenceList(validationTimelineEntries)
}
return timelines, nil
}
func (v featureLevelUniqueConstraintValidator) collectRelevantItems(subs []SubscriptionSpec, condition func(item *SubscriptionItemSpec) bool) map[string][]itemSpecWithCircularReferences {
relevantItems := make(map[string][]itemSpecWithCircularReferences)
for si := range subs {
sub := subs[si]
for pi := range sub.Phases {
phase := sub.Phases[pi]
for itemKey, items := range phase.ItemsByKey {
for idx := range items {
item := items[idx]
if condition(item) {
relevantItems[itemKey] = append(relevantItems[itemKey], itemSpecWithCircularReferences{
SubscriptionItemVersion: idx,
SubscriptionItemSpec: item,
SubscriptionPhaseSpec: phase,
SubscriptionSpec: &sub,
})
}
}
}
}
}
return relevantItems
}
func (v featureLevelUniqueConstraintValidator) itemIsRelevant(item *SubscriptionItemSpec) bool {
if item == nil {
return false
}
return v.itemHasEntitlements(item) || v.itemIsBillable(item)
}
func (v featureLevelUniqueConstraintValidator) itemHasEntitlements(item *SubscriptionItemSpec) bool {
if item == nil {
return false
}
return item.RateCard.AsMeta().EntitlementTemplate != nil
}
func (v featureLevelUniqueConstraintValidator) itemIsBillable(item *SubscriptionItemSpec) bool {
if item == nil {
return false
}
return item.RateCard.AsMeta().IsBillable()
}
// This will be a circular structure so be careful when using it
type itemSpecWithCircularReferences struct {
SubscriptionItemVersion int
SubscriptionItemSpec *SubscriptionItemSpec
SubscriptionPhaseSpec *SubscriptionPhaseSpec
SubscriptionSpec *SubscriptionSpec
}
func (i itemSpecWithCircularReferences) GetSelectors() *models.FieldDescriptor {
return models.NewFieldSelectorGroup(
subscriptionSpecToFieldSelectors(lo.FromPtr(i.SubscriptionSpec)),
models.NewFieldSelector("phases").WithExpression(models.NewFieldAttrValue("key", i.SubscriptionPhaseSpec.PhaseKey)),
models.NewFieldSelector("items").WithExpression(models.NewFieldAttrValue("key", i.SubscriptionItemSpec.ItemKey)),
models.NewFieldSelector("idx").WithExpression(models.NewFieldArrIndex(i.SubscriptionItemVersion)),
)
}
type validationTimelineEntry struct {
Item *itemSpecWithCircularReferences
Cadence models.CadencedModel
}
func (i validationTimelineEntry) GetCadence() models.CadencedModel {
return i.Cadence
}
func (i validationTimelineEntry) GetErrorAttributes(other validationTimelineEntry) models.Attributes {
return models.Attributes{
ErrCodeOnlySingleSubscriptionItemAllowedAtATime: SubscriptionFeatureLevelUniqueConstraintErrorDetail{
This: SubscriptionFeatureLevelUniqueConstraintErrorDetailSide{
Item: lo.FromPtr(i.Item.SubscriptionItemSpec),
PlanRef: lo.FromPtr(i.Item.SubscriptionSpec.Plan),
Cadence: i.GetCadence(),
Selectors: i.Item.GetSelectors(),
},
Other: SubscriptionFeatureLevelUniqueConstraintErrorDetailSide{
Item: lo.FromPtr(other.Item.SubscriptionItemSpec),
PlanRef: lo.FromPtr(other.Item.SubscriptionSpec.Plan),
Cadence: other.GetCadence(),
Selectors: other.Item.GetSelectors(),
},
},
}
}
func subscriptionSpecToFieldSelectors(subscriptionSpec SubscriptionSpec) *models.FieldDescriptor {
selectors := []*models.FieldDescriptor{}
if subscriptionSpec.Plan != nil {
selectors = append(selectors, planRefToFieldSelector(subscriptionSpec.Plan))
} else {
selectors = append(selectors, models.NewFieldSelector("plans"))
}
selectors = append(selectors,
models.NewFieldSelector("subscriptions").WithExpression(models.NewMultiFieldAttrValue(
func() []models.FieldAttrValue {
res := []models.FieldAttrValue{
models.NewFieldAttrValue("customerId", subscriptionSpec.CustomerId),
models.NewFieldAttrValue("activeFrom", subscriptionSpec.ActiveFrom),
}
if subscriptionSpec.ActiveTo != nil {
res = append(res, models.NewFieldAttrValue("activeTo", subscriptionSpec.ActiveTo))
}
return res
}()...,
)))
return models.NewFieldSelectorGroup(selectors...)
}
func planRefToFieldSelector(planRef *PlanRef) *models.FieldDescriptor {
if planRef == nil {
return models.NewFieldSelector("plans")
}
return models.NewFieldSelector("plans").WithExpression(models.NewMultiFieldAttrValue(
func() []models.FieldAttrValue {
res := []models.FieldAttrValue{}
if planRef.Key != "" {
res = append(res, models.NewFieldAttrValue("key", planRef.Key))
}
if planRef.Version != 0 {
res = append(res, models.NewFieldAttrValue("version", planRef.Version))
}
if planRef.Id != "" {
res = append(res, models.NewFieldAttrValue("id", planRef.Id))
}
return res
}()...,
))
}