Skip to content

Commit a05f908

Browse files
authored
Merge pull request #832 from tidepool-org/BACK-3514-out-of-order-bolus-food
[BACK-3514] Handle out-of-order food and bolus data for twiist
2 parents 2b1f38f + e80444e commit a05f908

File tree

12 files changed

+1065
-99
lines changed

12 files changed

+1065
-99
lines changed

data/data.go

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ const (
1616
)
1717

1818
type SelectorOrigin struct {
19-
ID *string `json:"id,omitempty"`
19+
ID *string `json:"id,omitempty" bson:"id,omitempty"`
20+
Time *string `json:"time,omitempty" bson:"time,omitempty"` // Inclusive, currently NOT used in database query
2021
}
2122

2223
func ParseSelectorOrigin(parser structure.ObjectParser) *SelectorOrigin {
@@ -34,15 +35,38 @@ func NewSelectorOrigin() *SelectorOrigin {
3435

3536
func (s *SelectorOrigin) Parse(parser structure.ObjectParser) {
3637
s.ID = parser.String("id")
38+
s.Time = parser.String("time")
3739
}
3840

3941
func (s *SelectorOrigin) Validate(validator structure.Validator) {
4042
validator.String("id", s.ID).Exists().NotEmpty().LengthLessThanOrEqualTo(SelectorOriginIDLengthMaximum)
43+
validator.String("time", s.Time).AsTime(time.RFC3339Nano).NotZero()
44+
}
45+
46+
func (s *SelectorOrigin) Includes(other *SelectorOrigin) bool {
47+
if s == nil || other == nil { // Must not be missing
48+
return false
49+
} else if s.ID != nil && (other.ID == nil || *s.ID != *other.ID) { // If id matters, then must include
50+
return false
51+
} else if s.Time == nil { // If time does not matter, success
52+
return true
53+
} else if other.Time == nil { // Must exist
54+
return false
55+
} else if sTime, err := time.Parse(time.RFC3339Nano, *s.Time); err != nil || sTime.IsZero() { // Must parse
56+
return false
57+
} else if otherTime, err := time.Parse(time.RFC3339Nano, *other.Time); err != nil || otherTime.IsZero() { // Must parse
58+
return false
59+
} else if otherTime.Before(sTime) { // Must include
60+
return false
61+
} else {
62+
return true
63+
}
4164
}
4265

4366
type Selector struct {
44-
ID *string `json:"id,omitempty"`
45-
Origin *SelectorOrigin `json:"origin,omitempty"`
67+
ID *string `json:"id,omitempty" bson:"id,omitempty"`
68+
Time *time.Time `json:"time,omitempty" bson:"time,omitempty"` // Inclusive, currently NOT used in database query
69+
Origin *SelectorOrigin `json:"origin,omitempty" bson:"origin,omitempty"`
4670
}
4771

4872
func ParseSelector(parser structure.ObjectParser) *Selector {
@@ -60,6 +84,7 @@ func NewSelector() *Selector {
6084

6185
func (s *Selector) Parse(parser structure.ObjectParser) {
6286
s.ID = parser.String("id")
87+
s.Time = parser.Time("time", TimeFormat)
6388
s.Origin = ParseSelectorOrigin(parser.WithReferenceObjectParser("origin"))
6489
}
6590

@@ -68,11 +93,27 @@ func (s *Selector) Validate(validator structure.Validator) {
6893
validator.ReportError(structureValidator.ErrorValuesNotExistForOne("id", "origin"))
6994
} else if s.ID != nil {
7095
validator.String("id", s.ID).Using(IDValidator)
96+
validator.Time("time", s.Time).NotZero()
7197
} else {
98+
validator.Time("time", s.Time).NotExists()
7299
s.Origin.Validate(validator.WithReference("origin"))
73100
}
74101
}
75102

103+
func (s *Selector) Includes(other *Selector) bool {
104+
if s == nil || other == nil { // Must not be missing
105+
return false
106+
} else if s.ID != nil && (other.ID == nil || *s.ID != *other.ID) { // If id matters, then must include
107+
return false
108+
} else if s.Time != nil && (other.Time == nil || other.Time.Before(*s.Time)) { // If time matters, then must include
109+
return false
110+
} else if s.Origin != nil && (other.Origin == nil || !s.Origin.Includes(other.Origin)) { // If origin matters, then must include
111+
return false
112+
} else {
113+
return true
114+
}
115+
}
116+
76117
type Selectors []*Selector
77118

78119
func ParseSelectors(parser structure.ArrayParser) *Selectors {
@@ -107,6 +148,16 @@ func (s *Selectors) Validate(validator structure.Validator) {
107148
}
108149
}
109150

151+
func (s *Selectors) Filter(predicate func(*Selector) bool) *Selectors {
152+
filtered := Selectors{}
153+
for _, selector := range *s {
154+
if predicate(selector) {
155+
filtered = append(filtered, selector)
156+
}
157+
}
158+
return &filtered
159+
}
160+
110161
func NewID() string {
111162
return id.Must(id.New(16))
112163
}

data/data_test.go

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,83 @@
11
package data_test
22

33
import (
4+
"time"
5+
46
. "github.com/onsi/ginkgo/v2"
57
. "github.com/onsi/gomega"
68

79
"github.com/tidepool-org/platform/data"
810
errorsTest "github.com/tidepool-org/platform/errors/test"
11+
"github.com/tidepool-org/platform/pointer"
912
structureTest "github.com/tidepool-org/platform/structure/test"
1013
structureValidator "github.com/tidepool-org/platform/structure/validator"
1114
"github.com/tidepool-org/platform/test"
1215
)
1316

1417
var _ = Describe("Data", func() {
18+
Context("SelectorOrigin", func() {
19+
Context("Includes", func() {
20+
now := time.Now()
21+
tm := pointer.FromString(now.Format(time.RFC3339Nano))
22+
id := pointer.FromString(data.NewID())
23+
24+
DescribeTable("return the expected results when the selector origins",
25+
func(origin *data.SelectorOrigin, otherOrigin *data.SelectorOrigin, expectedResult bool) {
26+
Expect(origin.Includes(otherOrigin)).To(Equal(expectedResult))
27+
},
28+
Entry("both are nil", nil, nil, false),
29+
Entry("origin is nil", nil, &data.SelectorOrigin{}, false),
30+
Entry("other origin is nil", &data.SelectorOrigin{}, nil, false),
31+
Entry("id and time are nil", &data.SelectorOrigin{}, &data.SelectorOrigin{}, true),
32+
Entry("origin id is nil", &data.SelectorOrigin{}, &data.SelectorOrigin{ID: id}, true),
33+
Entry("other origin id is nil", &data.SelectorOrigin{ID: id}, &data.SelectorOrigin{}, false),
34+
Entry("id mismatch", &data.SelectorOrigin{ID: id}, &data.SelectorOrigin{ID: pointer.FromString("mismatch")}, false),
35+
Entry("id includes", &data.SelectorOrigin{ID: id}, &data.SelectorOrigin{ID: id}, true),
36+
Entry("origin time is nil", &data.SelectorOrigin{ID: id}, &data.SelectorOrigin{ID: id, Time: tm}, true),
37+
Entry("other origin time is nil", &data.SelectorOrigin{ID: id, Time: tm}, &data.SelectorOrigin{ID: id}, false),
38+
Entry("time earlier", &data.SelectorOrigin{ID: id, Time: tm}, &data.SelectorOrigin{ID: id, Time: pointer.FromString(now.Add(-time.Hour).Format(time.RFC3339Nano))}, false),
39+
Entry("time same", &data.SelectorOrigin{ID: id, Time: tm}, &data.SelectorOrigin{ID: id, Time: tm}, true),
40+
Entry("time same in different time zone", &data.SelectorOrigin{ID: id, Time: tm}, &data.SelectorOrigin{ID: id, Time: pointer.FromString(now.In(time.FixedZone("Etc/GMT-1", int(-time.Hour.Seconds()))).Format(time.RFC3339Nano))}, true),
41+
Entry("time later", &data.SelectorOrigin{ID: id, Time: tm}, &data.SelectorOrigin{ID: id, Time: pointer.FromString(now.Add(time.Hour).Format(time.RFC3339Nano))}, true),
42+
)
43+
})
44+
})
45+
46+
Context("Selector", func() {
47+
Context("Includes", func() {
48+
now := time.Now()
49+
tm := pointer.FromTime(now)
50+
id := pointer.FromString(data.NewID())
51+
originID := pointer.FromString(data.NewID())
52+
53+
DescribeTable("return the expected results when the selector origins",
54+
func(origin *data.Selector, otherOrigin *data.Selector, expectedResult bool) {
55+
Expect(origin.Includes(otherOrigin)).To(Equal(expectedResult))
56+
},
57+
Entry("both are nil", nil, nil, false),
58+
Entry("selector is nil", nil, &data.Selector{}, false),
59+
Entry("other selector is nil", &data.Selector{}, nil, false),
60+
Entry("id, time, and origin are nil", &data.Selector{}, &data.Selector{}, true),
61+
Entry("selector id is nil", &data.Selector{}, &data.Selector{ID: id}, true),
62+
Entry("other selector id is nil", &data.Selector{ID: id}, &data.Selector{}, false),
63+
Entry("id mismatch", &data.Selector{ID: id}, &data.Selector{ID: pointer.FromString("mismatch")}, false),
64+
Entry("id includes", &data.Selector{ID: id}, &data.Selector{ID: id}, true),
65+
Entry("selector time is nil", &data.Selector{ID: id}, &data.Selector{ID: id, Time: tm}, true),
66+
Entry("other selector time is nil", &data.Selector{ID: id, Time: tm}, &data.Selector{ID: id}, false),
67+
Entry("time earlier", &data.Selector{ID: id, Time: tm}, &data.Selector{ID: id, Time: pointer.FromTime(now.Add(-time.Hour))}, false),
68+
Entry("time same", &data.Selector{ID: id, Time: tm}, &data.Selector{ID: id, Time: tm}, true),
69+
Entry("time same in different time zone", &data.Selector{ID: id, Time: tm}, &data.Selector{ID: id, Time: pointer.FromTime(now.In(time.FixedZone("Etc/GMT-1", int(-time.Hour.Seconds()))))}, true),
70+
Entry("time later", &data.Selector{ID: id, Time: tm}, &data.Selector{ID: id, Time: pointer.FromTime(now.Add(time.Hour))}, true),
71+
Entry("selector origin is nil", &data.Selector{ID: id}, &data.Selector{ID: id, Origin: &data.SelectorOrigin{ID: originID}}, true),
72+
Entry("other selector origin is nil", &data.Selector{ID: id, Origin: &data.SelectorOrigin{ID: originID}}, &data.Selector{ID: id}, false),
73+
Entry("origin id mismatch", &data.Selector{ID: id, Origin: &data.SelectorOrigin{ID: originID}}, &data.Selector{ID: id, Origin: &data.SelectorOrigin{ID: pointer.FromString("mismatch")}}, false),
74+
Entry("origin match", &data.Selector{ID: id, Origin: &data.SelectorOrigin{ID: originID}}, &data.Selector{ID: id, Origin: &data.SelectorOrigin{ID: originID}}, true),
75+
)
76+
})
77+
})
78+
1579
Context("NewID", func() {
16-
It("returns a string of 32 lowercase hexidecimal characters", func() {
80+
It("returns a string of 32 lowercase hexadecimal characters", func() {
1781
Expect(data.NewID()).To(MatchRegexp("^[0-9a-f]{32}$"))
1882
})
1983

data/datum.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,16 @@ func (d Data) SetModifiedTime(modifiedTime *time.Time) {
6060
}
6161
}
6262

63+
func (d Data) Filter(predicate func(Datum) bool) Data {
64+
filtered := Data{}
65+
for _, datum := range d {
66+
if predicate(datum) {
67+
filtered = append(filtered, datum)
68+
}
69+
}
70+
return filtered
71+
}
72+
6373
// Provenance of a document.
6474
//
6575
// Useful for determining additional actions to take. For example, if the

data/deduplicator/deduplicator/data_set_delete_origin.go

Lines changed: 24 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,26 @@ import (
66
"github.com/tidepool-org/platform/data"
77
dataStore "github.com/tidepool-org/platform/data/store"
88
dataTypesUpload "github.com/tidepool-org/platform/data/types/upload"
9-
"github.com/tidepool-org/platform/errors"
109
"github.com/tidepool-org/platform/pointer"
1110
)
1211

13-
const DataSetDeleteOriginName = "org.tidepool.deduplicator.dataset.delete.origin"
12+
const (
13+
DataSetDeleteOriginName = "org.tidepool.deduplicator.dataset.delete.origin"
14+
DataSetDeleteOriginVersion = "1.0.0"
15+
)
1416

1517
type DataSetDeleteOrigin struct {
16-
*Base
18+
*DataSetDeleteOriginBase
1719
}
1820

1921
func NewDataSetDeleteOrigin() (*DataSetDeleteOrigin, error) {
20-
base, err := NewBase(DataSetDeleteOriginName, "1.0.0")
22+
dataSetDeleteOriginBase, err := NewDataSetDeleteOriginBase(DataSetDeleteOriginName, DataSetDeleteOriginVersion, &dataSetDeleteOriginProvider{})
2123
if err != nil {
2224
return nil, err
2325
}
2426

2527
return &DataSetDeleteOrigin{
26-
Base: base,
28+
DataSetDeleteOriginBase: dataSetDeleteOriginBase,
2729
}, nil
2830
}
2931

@@ -32,106 +34,23 @@ func (d *DataSetDeleteOrigin) New(ctx context.Context, dataSet *dataTypesUpload.
3234
}
3335

3436
func (d *DataSetDeleteOrigin) Get(ctx context.Context, dataSet *dataTypesUpload.Upload) (bool, error) {
35-
if found, err := d.Base.Get(ctx, dataSet); err != nil || found {
37+
if found, err := d.DataSetDeleteOriginBase.Get(ctx, dataSet); err != nil || found {
3638
return found, err
3739
}
3840

3941
return dataSet.HasDeduplicatorNameMatch("org.tidepool.continuous.origin"), nil // TODO: DEPRECATED
4042
}
4143

42-
func (d *DataSetDeleteOrigin) Open(ctx context.Context, repository dataStore.DataRepository, dataSet *dataTypesUpload.Upload) (*dataTypesUpload.Upload, error) {
43-
if ctx == nil {
44-
return nil, errors.New("context is missing")
45-
}
46-
if repository == nil {
47-
return nil, errors.New("repository is missing")
48-
}
49-
if dataSet == nil {
50-
return nil, errors.New("data set is missing")
51-
}
52-
53-
if dataSet.HasDataSetTypeContinuous() {
54-
dataSet.SetActive(true)
55-
}
56-
57-
return d.Base.Open(ctx, repository, dataSet)
58-
}
59-
60-
func (d *DataSetDeleteOrigin) AddData(ctx context.Context, repository dataStore.DataRepository, dataSet *dataTypesUpload.Upload, dataSetData data.Data) error {
61-
if ctx == nil {
62-
return errors.New("context is missing")
63-
}
64-
if repository == nil {
65-
return errors.New("repository is missing")
66-
}
67-
if dataSet == nil {
68-
return errors.New("data set is missing")
69-
}
70-
if dataSetData == nil {
71-
return errors.New("data set data is missing")
72-
}
73-
74-
if dataSet.HasDataSetTypeContinuous() {
75-
dataSetData.SetActive(true)
76-
}
77-
78-
if selectors := d.getSelectors(dataSetData); selectors != nil {
79-
if err := repository.DeleteDataSetData(ctx, dataSet, selectors); err != nil {
80-
return err
81-
}
82-
if err := d.Base.AddData(ctx, repository, dataSet, dataSetData); err != nil {
83-
return err
84-
}
85-
return repository.DestroyDeletedDataSetData(ctx, dataSet, selectors)
86-
}
44+
type dataSetDeleteOriginProvider struct{}
8745

88-
return d.Base.AddData(ctx, repository, dataSet, dataSetData)
46+
func (d *dataSetDeleteOriginProvider) FilterData(ctx context.Context, repository dataStore.DataRepository, dataSet *dataTypesUpload.Upload, dataSetData data.Data) (data.Data, error) {
47+
return dataSetData, nil
8948
}
9049

91-
func (d *DataSetDeleteOrigin) DeleteData(ctx context.Context, repository dataStore.DataRepository, dataSet *dataTypesUpload.Upload, selectors *data.Selectors) error {
92-
if ctx == nil {
93-
return errors.New("context is missing")
94-
}
95-
if repository == nil {
96-
return errors.New("repository is missing")
97-
}
98-
if dataSet == nil {
99-
return errors.New("data set is missing")
100-
}
101-
if selectors == nil {
102-
return errors.New("selectors is missing")
103-
}
104-
105-
return repository.ArchiveDataSetData(ctx, dataSet, selectors)
106-
}
107-
108-
func (d *DataSetDeleteOrigin) Close(ctx context.Context, repository dataStore.DataRepository, dataSet *dataTypesUpload.Upload) error {
109-
if ctx == nil {
110-
return errors.New("context is missing")
111-
}
112-
if repository == nil {
113-
return errors.New("repository is missing")
114-
}
115-
if dataSet == nil {
116-
return errors.New("data set is missing")
117-
}
118-
119-
if dataSet.HasDataSetTypeContinuous() {
120-
return nil
121-
}
122-
123-
return d.Base.Close(ctx, repository, dataSet)
124-
}
125-
126-
func (d *DataSetDeleteOrigin) getSelectors(dataSetData data.Data) *data.Selectors {
50+
func (d *dataSetDeleteOriginProvider) GetDataSelectors(dataSetData data.Data) *data.Selectors {
12751
selectors := data.Selectors{}
12852
for _, dataSetDatum := range dataSetData {
129-
if origin := dataSetDatum.GetOrigin(); origin != nil && origin.ID != nil {
130-
selector := &data.Selector{
131-
Origin: &data.SelectorOrigin{
132-
ID: pointer.CloneString(origin.ID),
133-
},
134-
}
53+
if selector := d.getDatumSelector(dataSetDatum); selector != nil {
13554
selectors = append(selectors, selector)
13655
}
13756
}
@@ -140,3 +59,14 @@ func (d *DataSetDeleteOrigin) getSelectors(dataSetData data.Data) *data.Selector
14059
}
14160
return &selectors
14261
}
62+
63+
func (d *dataSetDeleteOriginProvider) getDatumSelector(dataSetDatum data.Datum) *data.Selector {
64+
if origin := dataSetDatum.GetOrigin(); origin != nil && origin.ID != nil {
65+
return &data.Selector{
66+
Origin: &data.SelectorOrigin{
67+
ID: pointer.CloneString(origin.ID),
68+
},
69+
}
70+
}
71+
return nil
72+
}

0 commit comments

Comments
 (0)