initial public commit

This commit is contained in:
Gani Georgiev
2022-07-07 00:19:05 +03:00
commit 3d07f0211d
484 changed files with 92412 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
package types
import (
"database/sql/driver"
"encoding/json"
"time"
"github.com/spf13/cast"
)
// DefaultDateLayout specifies the default app date strings layout.
const DefaultDateLayout = "2006-01-02 15:04:05.000"
// NowDateTime returns new DateTime instance with the current local time.
func NowDateTime() DateTime {
return DateTime{t: time.Now()}
}
// ParseDateTime creates a new DateTime from the provided value
// (could be [cast.ToTime] supported string, [time.Time], etc.).
func ParseDateTime(value any) (DateTime, error) {
d := DateTime{}
err := d.Scan(value)
return d, err
}
// DateTime represents a [time.Time] instance in UTC that is wrapped
// and serialized using the app default date layout.
type DateTime struct {
t time.Time
}
// Time returns the internal [time.Time] instance.
func (d DateTime) Time() time.Time {
return d.t
}
// IsZero checks whether the current DateTime instance has zero time value.
func (d DateTime) IsZero() bool {
return d.Time().IsZero()
}
// String serializes the current DateTime instance into a formated
// UTC date string.
//
// The zero value is serialized to an empty string.
func (d DateTime) String() string {
if d.IsZero() {
return ""
}
return d.Time().UTC().Format(DefaultDateLayout)
}
// MarshalJSON implements the [json.Marshaler] interface.
func (d DateTime) MarshalJSON() ([]byte, error) {
return json.Marshal(d.String())
}
// UnmarshalJSON implements the [json.Unmarshaler] interface.
func (d *DateTime) UnmarshalJSON(b []byte) error {
var raw string
if err := json.Unmarshal(b, &raw); err != nil {
return err
}
return d.Scan(raw)
}
// Value implements the [driver.Valuer] interface.
func (d DateTime) Value() (driver.Value, error) {
return d.String(), nil
}
// Scan implements [sql.Scanner] interface to scan the provided value
// into the current DateTime instance.
func (d *DateTime) Scan(value any) error {
switch v := value.(type) {
case DateTime:
d.t = v.Time()
case time.Time:
d.t = v
case int:
d.t = cast.ToTime(v)
default:
str := cast.ToString(v)
if str == "" {
d.t = time.Time{}
} else {
d.t = cast.ToTime(str)
}
}
return nil
}
+198
View File
@@ -0,0 +1,198 @@
package types_test
import (
"github.com/pocketbase/pocketbase/tools/types"
"strings"
"testing"
"time"
)
func TestNowDateTime(t *testing.T) {
now := time.Now().UTC().Format("2006-01-02 15:04:05") // without ms part for test consistency
dt := types.NowDateTime()
if !strings.Contains(dt.String(), now) {
t.Fatalf("Expected %q, got %q", now, dt.String())
}
}
func TestParseDateTime(t *testing.T) {
nowTime := time.Now().UTC()
nowDateTime, _ := types.ParseDateTime(nowTime)
nowStr := nowTime.Format(types.DefaultDateLayout)
scenarios := []struct {
value any
expected string
}{
{nil, ""},
{"", ""},
{"invalid", ""},
{nowDateTime, nowStr},
{nowTime, nowStr},
{1641024040, "2022-01-01 08:00:40.000"},
{"2022-01-01 11:23:45.678", "2022-01-01 11:23:45.678"},
}
for i, s := range scenarios {
dt, err := types.ParseDateTime(s.value)
if err != nil {
t.Errorf("(%d) Failed to parse %v: %v", i, s.value, err)
continue
}
if dt.String() != s.expected {
t.Errorf("(%d) Expected %q, got %q", i, s.expected, dt.String())
}
}
}
func TestDateTimeTime(t *testing.T) {
str := "2022-01-01 11:23:45.678"
expected, err := time.Parse(types.DefaultDateLayout, str)
if err != nil {
t.Fatal(err)
}
dt, err := types.ParseDateTime(str)
if err != nil {
t.Fatal(err)
}
result := dt.Time()
if !expected.Equal(result) {
t.Errorf("Expected time %v, got %v", expected, result)
}
}
func TestDateTimeIsZero(t *testing.T) {
dt0 := types.DateTime{}
if !dt0.IsZero() {
t.Fatalf("Expected zero datatime, got %v", dt0)
}
dt1 := types.NowDateTime()
if dt1.IsZero() {
t.Fatalf("Expected non-zero datatime, got %v", dt1)
}
}
func TestDateTimeString(t *testing.T) {
dt0 := types.DateTime{}
if dt0.String() != "" {
t.Fatalf("Expected empty string for zer datetime, got %q", dt0.String())
}
expected := "2022-01-01 11:23:45.678"
dt1, _ := types.ParseDateTime(expected)
if dt1.String() != expected {
t.Fatalf("Expected %q, got %v", expected, dt1)
}
}
func TestDateTimeMarshalJSON(t *testing.T) {
scenarios := []struct {
date string
expected string
}{
{"", `""`},
{"2022-01-01 11:23:45.678", `"2022-01-01 11:23:45.678"`},
}
for i, s := range scenarios {
dt, err := types.ParseDateTime(s.date)
if err != nil {
t.Errorf("(%d) %v", i, err)
}
result, err := dt.MarshalJSON()
if err != nil {
t.Errorf("(%d) %v", i, err)
}
if string(result) != s.expected {
t.Errorf("(%d) Expected %q, got %q", i, s.expected, string(result))
}
}
}
func TestDateTimeUnmarshalJSON(t *testing.T) {
scenarios := []struct {
date string
expected string
}{
{"", ""},
{"invalid_json", ""},
{"'123'", ""},
{"2022-01-01 11:23:45.678", ""},
{`"2022-01-01 11:23:45.678"`, "2022-01-01 11:23:45.678"},
}
for i, s := range scenarios {
dt := types.DateTime{}
dt.UnmarshalJSON([]byte(s.date))
if dt.String() != s.expected {
t.Errorf("(%d) Expected %q, got %q", i, s.expected, dt.String())
}
}
}
func TestDateTimeValue(t *testing.T) {
scenarios := []struct {
value any
expected string
}{
{"", ""},
{"invalid", ""},
{1641024040, "2022-01-01 08:00:40.000"},
{"2022-01-01 11:23:45.678", "2022-01-01 11:23:45.678"},
{types.NowDateTime(), types.NowDateTime().String()},
}
for i, s := range scenarios {
dt, _ := types.ParseDateTime(s.value)
result, err := dt.Value()
if err != nil {
t.Errorf("(%d) %v", i, err)
continue
}
if result != s.expected {
t.Errorf("(%d) Expected %q, got %q", i, s.expected, result)
}
}
}
func TestDateTimeScan(t *testing.T) {
now := time.Now().UTC().Format("2006-01-02 15:04:05") // without ms part for test consistency
scenarios := []struct {
value any
expected string
}{
{nil, ""},
{"", ""},
{"invalid", ""},
{types.NowDateTime(), now},
{time.Now(), now},
{1641024040, "2022-01-01 08:00:40.000"},
{"2022-01-01 11:23:45.678", "2022-01-01 11:23:45.678"},
}
for i, s := range scenarios {
dt := types.DateTime{}
err := dt.Scan(s.value)
if err != nil {
t.Errorf("(%d) Failed to parse %v: %v", i, s.value, err)
continue
}
if !strings.Contains(dt.String(), s.expected) {
t.Errorf("(%d) Expected %q, got %q", i, s.expected, dt.String())
}
}
}
+55
View File
@@ -0,0 +1,55 @@
package types
import (
"database/sql/driver"
"encoding/json"
"fmt"
)
// JsonArray defines a slice that is safe for json and db read/write.
type JsonArray []any
// MarshalJSON implements the [json.Marshaler] interface.
func (m JsonArray) MarshalJSON() ([]byte, error) {
type alias JsonArray // prevent recursion
// inialize an empty map to ensure that `[]` is returned as json
if m == nil {
m = JsonArray{}
}
return json.Marshal(alias(m))
}
// Value implements the [driver.Valuer] interface.
func (m JsonArray) Value() (driver.Value, error) {
if m == nil {
return nil, nil
}
data, err := json.Marshal(m)
return string(data), err
}
// Scan implements [sql.Scanner] interface to scan the provided value
// into the current `JsonArray` instance.
func (m *JsonArray) Scan(value any) error {
var data []byte
switch v := value.(type) {
case nil:
// no cast needed
case []byte:
data = v
case string:
data = []byte(v)
default:
return fmt.Errorf("Failed to unmarshal JsonArray value: %q.", value)
}
if len(data) == 0 {
data = []byte("[]")
}
return json.Unmarshal(data, m)
}
+95
View File
@@ -0,0 +1,95 @@
package types_test
import (
"database/sql/driver"
"testing"
"github.com/pocketbase/pocketbase/tools/types"
)
func TestJsonArrayMarshalJSON(t *testing.T) {
scenarios := []struct {
json types.JsonArray
expected string
}{
{nil, "[]"},
{types.JsonArray{}, `[]`},
{types.JsonArray{1, 2, 3}, `[1,2,3]`},
{types.JsonArray{"test1", "test2", "test3"}, `["test1","test2","test3"]`},
{types.JsonArray{1, "test"}, `[1,"test"]`},
}
for i, s := range scenarios {
result, err := s.json.MarshalJSON()
if err != nil {
t.Errorf("(%d) %v", i, err)
continue
}
if string(result) != s.expected {
t.Errorf("(%d) Expected %s, got %s", i, s.expected, string(result))
}
}
}
func TestJsonArrayValue(t *testing.T) {
scenarios := []struct {
json types.JsonArray
expected driver.Value
}{
{nil, nil},
{types.JsonArray{}, `[]`},
{types.JsonArray{1, 2, 3}, `[1,2,3]`},
{types.JsonArray{"test1", "test2", "test3"}, `["test1","test2","test3"]`},
{types.JsonArray{1, "test"}, `[1,"test"]`},
}
for i, s := range scenarios {
result, err := s.json.Value()
if err != nil {
t.Errorf("(%d) %v", i, err)
continue
}
if result != s.expected {
t.Errorf("(%d) Expected %s, got %v", i, s.expected, result)
}
}
}
func TestJsonArrayScan(t *testing.T) {
scenarios := []struct {
value any
expectError bool
expectJson string
}{
{``, false, `[]`},
{[]byte{}, false, `[]`},
{nil, false, `[]`},
{123, true, `[]`},
{`""`, true, `[]`},
{`invalid_json`, true, `[]`},
{`"test"`, true, `[]`},
{`1,2,3`, true, `[]`},
{`[1, 2, 3`, true, `[]`},
{`[1, 2, 3]`, false, `[1,2,3]`},
{[]byte(`[1, 2, 3]`), false, `[1,2,3]`},
{`[1, "test"]`, false, `[1,"test"]`},
{`[]`, false, `[]`},
}
for i, s := range scenarios {
arr := types.JsonArray{}
scanErr := arr.Scan(s.value)
hasErr := scanErr != nil
if hasErr != s.expectError {
t.Errorf("(%d) Expected %v, got %v (%v)", i, s.expectError, hasErr, scanErr)
continue
}
result, _ := arr.MarshalJSON()
if string(result) != s.expectJson {
t.Errorf("(%d) Expected %s, got %v", i, s.expectJson, string(result))
}
}
}
+55
View File
@@ -0,0 +1,55 @@
package types
import (
"database/sql/driver"
"encoding/json"
"fmt"
)
// JsonMap defines a map that is safe for json and db read/write.
type JsonMap map[string]any
// MarshalJSON implements the [json.Marshaler] interface.
func (m JsonMap) MarshalJSON() ([]byte, error) {
type alias JsonMap // prevent recursion
// inialize an empty map to ensure that `{}` is returned as json
if m == nil {
m = JsonMap{}
}
return json.Marshal(alias(m))
}
// Value implements the [driver.Valuer] interface.
func (m JsonMap) Value() (driver.Value, error) {
if m == nil {
return nil, nil
}
data, err := json.Marshal(m)
return string(data), err
}
// Scan implements [sql.Scanner] interface to scan the provided value
// into the current `JsonMap` instance.
func (m *JsonMap) Scan(value any) error {
var data []byte
switch v := value.(type) {
case nil:
// no cast needed
case []byte:
data = v
case string:
data = []byte(v)
default:
return fmt.Errorf("Failed to unmarshal JsonMap value: %q.", value)
}
if len(data) == 0 {
data = []byte("{}")
}
return json.Unmarshal(data, m)
}
+92
View File
@@ -0,0 +1,92 @@
package types_test
import (
"database/sql/driver"
"testing"
"github.com/pocketbase/pocketbase/tools/types"
)
func TestJsonMapMarshalJSON(t *testing.T) {
scenarios := []struct {
json types.JsonMap
expected string
}{
{nil, "{}"},
{types.JsonMap{}, `{}`},
{types.JsonMap{"test1": 123, "test2": "lorem"}, `{"test1":123,"test2":"lorem"}`},
{types.JsonMap{"test": []int{1, 2, 3}}, `{"test":[1,2,3]}`},
}
for i, s := range scenarios {
result, err := s.json.MarshalJSON()
if err != nil {
t.Errorf("(%d) %v", i, err)
continue
}
if string(result) != s.expected {
t.Errorf("(%d) Expected %s, got %s", i, s.expected, string(result))
}
}
}
func TestJsonMapValue(t *testing.T) {
scenarios := []struct {
json types.JsonMap
expected driver.Value
}{
{nil, nil},
{types.JsonMap{}, `{}`},
{types.JsonMap{"test1": 123, "test2": "lorem"}, `{"test1":123,"test2":"lorem"}`},
{types.JsonMap{"test": []int{1, 2, 3}}, `{"test":[1,2,3]}`},
}
for i, s := range scenarios {
result, err := s.json.Value()
if err != nil {
t.Errorf("(%d) %v", i, err)
continue
}
if result != s.expected {
t.Errorf("(%d) Expected %s, got %v", i, s.expected, result)
}
}
}
func TestJsonArrayMapScan(t *testing.T) {
scenarios := []struct {
value any
expectError bool
expectJson string
}{
{``, false, `{}`},
{nil, false, `{}`},
{[]byte{}, false, `{}`},
{`{}`, false, `{}`},
{123, true, `{}`},
{`""`, true, `{}`},
{`invalid_json`, true, `{}`},
{`"test"`, true, `{}`},
{`1,2,3`, true, `{}`},
{`{"test": 1`, true, `{}`},
{`{"test": 1}`, false, `{"test":1}`},
{[]byte(`{"test": 1}`), false, `{"test":1}`},
}
for i, s := range scenarios {
arr := types.JsonMap{}
scanErr := arr.Scan(s.value)
hasErr := scanErr != nil
if hasErr != s.expectError {
t.Errorf("(%d) Expected %v, got %v (%v)", i, s.expectError, hasErr, scanErr)
continue
}
result, _ := arr.MarshalJSON()
if string(result) != s.expectJson {
t.Errorf("(%d) Expected %s, got %v", i, s.expectJson, string(result))
}
}
}
+83
View File
@@ -0,0 +1,83 @@
package types
import (
"database/sql/driver"
"encoding/json"
"errors"
)
// JsonRaw defines a json value type that is safe for db read/write.
type JsonRaw []byte
// ParseJsonRaw creates a new JsonRaw instance from the provided value
// (could be JsonRaw, int, float, string, []byte, etc.).
func ParseJsonRaw(value any) (JsonRaw, error) {
result := JsonRaw{}
err := result.Scan(value)
return result, err
}
// String returns the current JsonRaw instance as a json encoded string.
func (j JsonRaw) String() string {
return string(j)
}
// MarshalJSON implements the [json.Marshaler] interface.
func (j JsonRaw) MarshalJSON() ([]byte, error) {
if len(j) == 0 {
return []byte("null"), nil
}
return j, nil
}
// UnmarshalJSON implements the [json.Unmarshaler] interface.
func (j *JsonRaw) UnmarshalJSON(b []byte) error {
if j == nil {
return errors.New("JsonRaw: UnmarshalJSON on nil pointer")
}
*j = append((*j)[0:0], b...)
return nil
}
// Value implements the [driver.Valuer] interface.
func (j JsonRaw) Value() (driver.Value, error) {
if len(j) == 0 {
return nil, nil
}
return j.String(), nil
}
// Scan implements [sql.Scanner] interface to scan the provided value
// into the current JsonRaw instance.
func (j *JsonRaw) Scan(value interface{}) error {
var data []byte
switch v := value.(type) {
case nil:
// no cast is needed
case []byte:
if len(v) != 0 {
data = v
}
case string:
if v != "" {
data = []byte(v)
}
case JsonRaw:
if len(v) != 0 {
data = []byte(v)
}
default:
bytes, err := json.Marshal(v)
if err != nil {
return err
}
data = bytes
}
return j.UnmarshalJSON(data)
}
+178
View File
@@ -0,0 +1,178 @@
package types_test
import (
"database/sql/driver"
"testing"
"github.com/pocketbase/pocketbase/tools/types"
)
func TestParseJsonRaw(t *testing.T) {
scenarios := []struct {
value any
expectError bool
expectJson string
}{
{nil, false, `null`},
{``, false, `null`},
{[]byte{}, false, `null`},
{types.JsonRaw{}, false, `null`},
{`{}`, false, `{}`},
{`[]`, false, `[]`},
{123, false, `123`},
{`""`, false, `""`},
{`test`, false, `test`},
{`{"invalid"`, false, `{"invalid"`}, // treated as a byte casted string
{`{"test":1}`, false, `{"test":1}`},
{[]byte(`[1,2,3]`), false, `[1,2,3]`},
{[]int{1, 2, 3}, false, `[1,2,3]`},
{map[string]int{"test": 1}, false, `{"test":1}`},
}
for i, s := range scenarios {
raw, parseErr := types.ParseJsonRaw(s.value)
hasErr := parseErr != nil
if hasErr != s.expectError {
t.Errorf("(%d) Expected %v, got %v (%v)", i, s.expectError, hasErr, parseErr)
continue
}
result, _ := raw.MarshalJSON()
if string(result) != s.expectJson {
t.Errorf("(%d) Expected %s, got %v", i, s.expectJson, string(result))
}
}
}
func TestJsonRawString(t *testing.T) {
scenarios := []struct {
json types.JsonRaw
expected string
}{
{nil, ``},
{types.JsonRaw{}, ``},
{types.JsonRaw([]byte(`123`)), `123`},
{types.JsonRaw(`{"demo":123}`), `{"demo":123}`},
}
for i, s := range scenarios {
result := s.json.String()
if result != s.expected {
t.Errorf("(%d) Expected %q, got %q", i, s.expected, result)
}
}
}
func TestJsonRawMarshalJSON(t *testing.T) {
scenarios := []struct {
json types.JsonRaw
expected string
}{
{nil, `null`},
{types.JsonRaw{}, `null`},
{types.JsonRaw([]byte(`123`)), `123`},
{types.JsonRaw(`{"demo":123}`), `{"demo":123}`},
}
for i, s := range scenarios {
result, err := s.json.MarshalJSON()
if err != nil {
t.Errorf("(%d) %v", i, err)
continue
}
if string(result) != s.expected {
t.Errorf("(%d) Expected %q, got %q", i, s.expected, string(result))
}
}
}
func TestJsonRawUnmarshalJSON(t *testing.T) {
scenarios := []struct {
json []byte
expectString string
}{
{nil, ""},
{[]byte{0, 1, 2}, "\x00\x01\x02"},
{[]byte("123"), "123"},
{[]byte("test"), "test"},
{[]byte(`{"test":123}`), `{"test":123}`},
}
for i, s := range scenarios {
raw := types.JsonRaw{}
err := raw.UnmarshalJSON(s.json)
if err != nil {
t.Errorf("(%d) %v", i, err)
continue
}
if raw.String() != s.expectString {
t.Errorf("(%d) Expected %q, got %q", i, s.expectString, raw.String())
}
}
}
func TestJsonRawValue(t *testing.T) {
scenarios := []struct {
json types.JsonRaw
expected driver.Value
}{
{nil, nil},
{types.JsonRaw{}, nil},
{types.JsonRaw(``), nil},
{types.JsonRaw(`test`), `test`},
}
for i, s := range scenarios {
result, err := s.json.Value()
if err != nil {
t.Errorf("(%d) %v", i, err)
continue
}
if result != s.expected {
t.Errorf("(%d) Expected %s, got %v", i, s.expected, result)
}
}
}
func TestJsonRawScan(t *testing.T) {
scenarios := []struct {
value any
expectError bool
expectJson string
}{
{nil, false, `null`},
{``, false, `null`},
{[]byte{}, false, `null`},
{types.JsonRaw{}, false, `null`},
{types.JsonRaw(`test`), false, `test`},
{`{}`, false, `{}`},
{`[]`, false, `[]`},
{123, false, `123`},
{`""`, false, `""`},
{`test`, false, `test`},
{`{"invalid"`, false, `{"invalid"`}, // treated as a byte casted string
{`{"test":1}`, false, `{"test":1}`},
{[]byte(`[1,2,3]`), false, `[1,2,3]`},
{[]int{1, 2, 3}, false, `[1,2,3]`},
{map[string]int{"test": 1}, false, `{"test":1}`},
}
for i, s := range scenarios {
raw := types.JsonRaw{}
scanErr := raw.Scan(s.value)
hasErr := scanErr != nil
if hasErr != s.expectError {
t.Errorf("(%d) Expected %v, got %v (%v)", i, s.expectError, hasErr, scanErr)
continue
}
result, _ := raw.MarshalJSON()
if string(result) != s.expectJson {
t.Errorf("(%d) Expected %s, got %v", i, s.expectJson, string(result))
}
}
}