Skip to content

Commit

Permalink
tjson: Implement datetime (#1027)
Browse files Browse the repository at this point in the history
Closes #905.

Co-authored-by: Alexey Palazhchenko <alexey.palazhchenko@ferretdb.io>
  • Loading branch information
noisersup and AlekSi committed Aug 12, 2022
1 parent e6a4e8f commit c8f6b78
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 9 deletions.
4 changes: 2 additions & 2 deletions integration/shareddata/shareddata.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ func AllProviders() []Provider {
Binaries,
ObjectIDs,
Bools,
// DateTimes, TODO https://github.com/FerretDB/FerretDB/issues/905
DateTimes,
// Nulls, TODO https://github.com/FerretDB/FerretDB/issues/906
// Regexes, TODO https://github.com/FerretDB/FerretDB/issues/911
Int32s,
// Timestamps, TODO https://github.com/FerretDB/FerretDB/issues/905
// Timestamps, TODO https://github.com/FerretDB/FerretDB/issues/1007
Int64s,
// Unsets, TODO https://github.com/FerretDB/FerretDB/issues/1023

Expand Down
72 changes: 72 additions & 0 deletions internal/tjson/datetime.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright 2021 FerretDB Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package tjson

import (
"bytes"
"encoding/json"
"time"

"github.com/FerretDB/FerretDB/internal/util/lazyerrors"
)

// dateTimeType represents BSON UTC datetime type.
type dateTimeType time.Time

// tjsontype implements tjsontype interface.
func (dt *dateTimeType) tjsontype() {}

// String returns formatted time for debugging.
func (dt *dateTimeType) String() string {
return time.Time(*dt).Format(time.RFC3339Nano)
}

// UnmarshalJSON implements the json.Unmarshaler interface.
func (dt *dateTimeType) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, []byte("null")) {
panic("null data")
}

r := bytes.NewReader(data)
dec := json.NewDecoder(r)

var o time.Time
if err := dec.Decode(&o); err != nil {
return lazyerrors.Error(err)
}

if err := checkConsumed(dec, r); err != nil {
return lazyerrors.Error(err)
}

*dt = dateTimeType(time.UnixMilli(o.UnixMilli()).UTC())

return nil
}

// MarshalJSON implements tjsontype interface.
func (dt *dateTimeType) MarshalJSON() ([]byte, error) {
res, err := json.Marshal(time.UnixMilli(time.Time(*dt).UnixMilli()).UTC())
if err != nil {
return nil, lazyerrors.Error(err)
}

return res, nil
}

// check interfaces
var (
_ tjsontype = (*dateTimeType)(nil)
)
62 changes: 62 additions & 0 deletions internal/tjson/datetime_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright 2021 FerretDB Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package tjson

import (
"testing"
"time"

"github.com/AlekSi/pointer"
)

var dateTimeTestCases = []testCase{{
name: "2021",
v: pointer.To(dateTimeType(time.Date(2021, 11, 1, 10, 18, 42, 123000000, time.UTC))),
schema: dateTimeSchema,
j: `"2021-11-01T10:18:42.123Z"`,
}, {
name: "unix_zero",
v: pointer.To(dateTimeType(time.Unix(0, 0).UTC())),
schema: dateTimeSchema,
j: `"1970-01-01T00:00:00Z"`,
}, {
name: "0",
v: pointer.To(dateTimeType(time.Date(0, 1, 1, 0, 0, 0, 0, time.UTC))),
schema: dateTimeSchema,
j: `"0000-01-01T00:00:00Z"`,
}, {
name: "9999",
v: pointer.To(dateTimeType(time.Date(9999, 12, 31, 23, 59, 59, 999000000, time.UTC))),
schema: dateTimeSchema,
j: `"9999-12-31T23:59:59.999Z"`,
}, {
name: "EOF",
schema: stringSchema,
j: `{`,
jErr: `unexpected EOF`,
}}

func TestDateTime(t *testing.T) {
t.Parallel()
testJSON(t, dateTimeTestCases, func() tjsontype { return new(dateTimeType) })
}

func FuzzDateTime(f *testing.F) {
fuzzJSON(f, dateTimeTestCases)
}

func BenchmarkDateTime(b *testing.B) {
benchmark(b, dateTimeTestCases)
}
3 changes: 1 addition & 2 deletions internal/tjson/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,7 @@ func valueSchema(v any) (*Schema, error) {
case bool:
return boolSchema, nil
case time.Time:
// return dateTimeSchema, nil
return nil, lazyerrors.Errorf("%T is not supported yet", v)
return dateTimeSchema, nil
case types.NullType:
return nil, lazyerrors.Errorf("%T is not supported yet", v)
case types.Regex:
Expand Down
15 changes: 10 additions & 5 deletions internal/tjson/tjson.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"encoding/json"
"fmt"
"io"
"time"

"github.com/AlekSi/pointer"

Expand Down Expand Up @@ -70,8 +71,8 @@ func fromTJSON(v tjsontype) any {
return types.ObjectID(*v)
case *boolType:
return bool(*v)
// case *dateTimeType:
// return time.Time(*v)
case *dateTimeType:
return time.Time(*v)
// case *nullType:
// return types.Null
// case *regexType:
Expand Down Expand Up @@ -104,8 +105,8 @@ func toTJSON(v any) tjsontype {
return pointer.To(objectIDType(v))
case bool:
return pointer.To(boolType(v))
// case time.Time:
// return pointer.To(dateTimeType(v))
case time.Time:
return pointer.To(dateTimeType(v))
// case types.NullType:
// return pointer.To(nullType(v))
// case types.Regex:
Expand Down Expand Up @@ -168,7 +169,11 @@ func Unmarshal(data []byte, schema *Schema) (any, error) {
var o objectIDType
err = o.UnmarshalJSON(data)
res = &o
case UUID, DateTime:
case DateTime:
var o dateTimeType
err = o.UnmarshalJSON(data)
res = &o
case UUID:
fallthrough
case Double, Float, Int64, Int32:
fallthrough
Expand Down
2 changes: 2 additions & 0 deletions internal/tjson/tjson_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,8 @@ func unmarshalJSON(v tjsontype, j string) (bool, error) {
err = v.UnmarshalJSON([]byte(j))
case *boolType:
err = v.UnmarshalJSON([]byte(j))
case *dateTimeType:
err = v.UnmarshalJSON([]byte(j))
case *int32Type:
err = v.UnmarshalJSON([]byte(j))
case *int64Type:
Expand Down

0 comments on commit c8f6b78

Please sign in to comment.