File size: 6,454 Bytes
d6f631f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | package patch
import (
"fmt"
"github.com/openmeterio/openmeter/openmeter/subscription"
"github.com/openmeterio/openmeter/pkg/datetime"
"github.com/openmeterio/openmeter/pkg/models"
)
type PatchAddItem struct {
PhaseKey string
ItemKey string
CreateInput subscription.SubscriptionItemSpec
}
func (a PatchAddItem) Op() subscription.PatchOperation {
return subscription.PatchOperationAdd
}
func (a PatchAddItem) Path() subscription.SpecPath {
return subscription.NewItemPath(a.PhaseKey, a.ItemKey)
}
func (a PatchAddItem) Value() subscription.SubscriptionItemSpec {
return a.CreateInput
}
func (a PatchAddItem) Validate() error {
if err := a.Path().Validate(); err != nil {
return err
}
if err := a.Op().Validate(); err != nil {
return err
}
if err := a.CreateInput.Validate(); err != nil {
return models.ErrorWithFieldPrefix(a.FieldDescriptor(), err)
}
return nil
}
func (a PatchAddItem) FieldDescriptor() *models.FieldDescriptor {
return models.NewFieldSelectorGroup(
models.NewFieldSelectorGroup(
models.NewFieldSelector("phases"),
models.NewFieldSelector(a.PhaseKey),
).WithAttributes(models.Attributes{
subscription.PhaseDescriptor: true,
}),
models.NewFieldSelector("items"),
models.NewFieldSelector(a.ItemKey),
)
}
func (a PatchAddItem) ValueAsAny() any {
return a.CreateInput
}
var _ subscription.ValuePatch[subscription.SubscriptionItemSpec] = PatchAddItem{}
func (a PatchAddItem) ApplyTo(spec *subscription.SubscriptionSpec, actx subscription.ApplyContext) error {
phase, ok := spec.Phases[a.PhaseKey]
if !ok {
return &subscription.PatchValidationError{Msg: fmt.Sprintf("phase %s not found", a.PhaseKey)}
}
phaseStartTime, _ := phase.StartAfter.AddTo(spec.ActiveFrom)
// Checks we need:
// 1. You cannot add items to previous phases
currentPhase, exists := spec.GetCurrentPhaseAt(actx.CurrentTime)
if !exists {
// If the current phase doesn't exist then either all phases are in the past or in the future
// If all phases are in the past then no addition is possible
// If all phases are in the past then the selected one is also in the past
if st, _ := phase.StartAfter.AddTo(spec.ActiveFrom); st.Before(actx.CurrentTime) {
return &subscription.PatchForbiddenError{Msg: fmt.Sprintf("cannot add item to phase %s which starts before current phase", a.PhaseKey)}
} else {
// If it's added to a future phase, the matching key for the phase has to be empty
if len(phase.ItemsByKey) > 0 {
return &subscription.PatchForbiddenError{Msg: fmt.Sprintf("cannot add item to future phase %s which already has items", a.PhaseKey)}
}
}
} else {
currentPhaseStartTime, _ := currentPhase.StartAfter.AddTo(spec.ActiveFrom)
// If the selected phase is before the current phase, it's forbidden
if phaseStartTime.Before(currentPhaseStartTime) {
return &subscription.PatchForbiddenError{Msg: fmt.Sprintf("cannot add item to phase %s which starts before current phase", a.PhaseKey)}
} else if phase.PhaseKey == currentPhase.PhaseKey {
// Sanity check
if actx.CurrentTime.Before(phaseStartTime) {
return fmt.Errorf("current time is before the current phase start which is impossible")
}
// 2. If it's added to the current phase, the specified start time cannot point to the past
if a.CreateInput.ActiveFromOverrideRelativeToPhaseStart != nil {
iST, _ := a.CreateInput.ActiveFromOverrideRelativeToPhaseStart.AddTo(phaseStartTime)
if iST.Before(actx.CurrentTime) {
return &subscription.PatchForbiddenError{Msg: fmt.Sprintf("cannot add item to phase %s which would become active in the past at %s", a.PhaseKey, iST)}
}
} else {
// 3. If it's added to the current phase, and start time is not specified, it will be set for the current time, as you cannot change the past
diff := datetime.ISODurationBetween(phaseStartTime, actx.CurrentTime)
a.CreateInput.ActiveFromOverrideRelativeToPhaseStart = &diff
}
} else if phaseStartTime.After(currentPhaseStartTime) {
// 4. If you're adding it to a future phase, the matching key for the phase has to be empty
if len(phase.ItemsByKey[a.ItemKey]) > 0 {
return &subscription.PatchForbiddenError{Msg: fmt.Sprintf("cannot add item to future phase %s which already has items", a.PhaseKey)}
}
} else {
return fmt.Errorf("didn't enter any logical branch")
}
}
// Finally, let's try to add it to the phase
if phase.ItemsByKey[a.ItemKey] == nil {
phase.ItemsByKey[a.ItemKey] = make([]*subscription.SubscriptionItemSpec, 0)
}
// If it's added to the current phase, we need to close the activity of any current item if present
hasCurrentItemAndShouldCloseCurrentItemForKey := false
if exists && currentPhase.PhaseKey == phase.PhaseKey {
if len(phase.ItemsByKey[a.ItemKey]) > 0 {
hasCurrentItemAndShouldCloseCurrentItemForKey = true
}
}
if hasCurrentItemAndShouldCloseCurrentItemForKey {
// Sanity check
if len(phase.ItemsByKey[a.ItemKey]) == 0 {
return fmt.Errorf("there should be an item to close")
}
itemToClose := phase.ItemsByKey[a.ItemKey][len(phase.ItemsByKey[a.ItemKey])-1]
// If it already has a scheduled end time, which is later than the time this new item should start, we should error.
// The user can circumvent this, by first issuing a delete for the item, and then adding a new one.
if itemToClose.ActiveToOverrideRelativeToPhaseStart != nil {
itemToCloseEndTime, _ := itemToClose.ActiveToOverrideRelativeToPhaseStart.AddTo(phaseStartTime)
// Sanity check
if a.CreateInput.ActiveFromOverrideRelativeToPhaseStart == nil {
return fmt.Errorf("ActiveFromOverrideRelativeToPhaseStart should already be set when adding after an already existing item for the current phase")
}
itemToAddStartTime, _ := a.CreateInput.ActiveFromOverrideRelativeToPhaseStart.AddTo(phaseStartTime)
if itemToCloseEndTime.After(itemToAddStartTime) {
return &subscription.PatchForbiddenError{Msg: fmt.Sprintf("cannot add item to phase %s which would overlap with a current item, you should delete first", a.PhaseKey)}
}
}
// Let's update the current item to close to actually close as the new item starts
itemToClose.ActiveToOverrideRelativeToPhaseStart = a.CreateInput.ActiveFromOverrideRelativeToPhaseStart
}
// Finally, we simply add it as the last Spec for its key in the phase
phase.ItemsByKey[a.ItemKey] = append(phase.ItemsByKey[a.ItemKey], &a.CreateInput)
return nil
}
|