Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/docs/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ Those types are not supported yet, but will be in the future:

`models.SliceField` can be used to store slices, that are encoded to JSON string for storage (implement sql.Scanner and driver.Valuer interfaces). In request and response JSON payloads, the slice is represented as a JSON array. The types of the slice need to be golang built-in basic types. The field provides validation of all the elements in the slice.

### JSON fields

`datatypes.JSON` from `gorm.datatypes` package can be used to store JSON data in a database, in JSON column type native to the database. The field is represented as a JSON object in the request and response JSON payloads.

## Model relations

GRF models by themselves do not directly support relations, but:
Expand Down
7 changes: 5 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,26 @@ require (
github.com/shopspring/decimal v1.3.1
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.10.0
gorm.io/datatypes v1.2.5
gorm.io/driver/postgres v1.5.11
gorm.io/driver/sqlite v1.5.2
gorm.io/gorm v1.25.12
)

require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/bytedance/sonic v1.12.8 // indirect
github.com/bytedance/sonic/loader v0.2.3 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
Expand Down Expand Up @@ -60,4 +62,5 @@ require (
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/mysql v1.5.6 // indirect
)
78 changes: 20 additions & 58 deletions go.sum

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions pkg/detectors/attributes.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/glothriel/grf/pkg/fields"
"github.com/glothriel/grf/pkg/models"
"gorm.io/datatypes"
)

// Prints a summary with the fields of the model obtained using reflection
Expand Down Expand Up @@ -56,7 +57,8 @@ type fieldSettings struct {

isGRFRepresentable bool
isGRFParsable bool
isForeignKey bool
isRelation bool
isDataTypesJSON bool

isSqlNullInt32 bool
}
Expand Down Expand Up @@ -84,6 +86,7 @@ func getFieldSettings[Model any](fieldName string) *fieldSettings {
_, isGRFRepresentable := theTypeAsAny.(fields.GRFRepresentable)
_, isGRFParsable := theTypeAsAny.(fields.GRFParsable)
_, isSQLNull32 := theTypeAsAny.(*sql.NullInt32)
_, isDataTypesJSON := theTypeAsAny.(*datatypes.JSON)

settings = &fieldSettings{
itsType: reflect.TypeOf(
Expand All @@ -93,7 +96,8 @@ func getFieldSettings[Model any](fieldName string) *fieldSettings {
isEncodingTextUnmarshaler: isEncodingTextUnmarshaler,
isGRFRepresentable: isGRFRepresentable,
isGRFParsable: isGRFParsable,
isForeignKey: fieldMarkedAsRelation,
isRelation: fieldMarkedAsRelation,
isDataTypesJSON: isDataTypesJSON,
isSqlNullInt32: isSQLNull32,
}
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/detectors/detectors.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type relationshipDetector[Model any] struct {

func (p *relationshipDetector[Model]) ToInternalValue(fieldName string) (fields.InternalValueFunc, error) {
fieldSettings := getFieldSettings[Model](fieldName)
if fieldSettings.isForeignKey {
if fieldSettings.isRelation {
return nil, ErrFieldShouldBeSkipped
}
return p.internalChild.ToInternalValue(fieldName)
Expand All @@ -28,7 +28,7 @@ func (p *relationshipDetector[Model]) ToRepresentation(fieldName string) (fields
return nil, fmt.Errorf("Field `%s` is not present in the model", fieldName)

}
if fieldSettings.isForeignKey {
if fieldSettings.isRelation {
return nil, ErrFieldShouldBeSkipped
}
return p.representationChild.ToRepresentation(fieldName)
Expand Down
28 changes: 28 additions & 0 deletions pkg/detectors/internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package detectors
import (
"database/sql"
"encoding"
"encoding/json"
"fmt"
"reflect"
"time"

"github.com/gin-gonic/gin"
"github.com/glothriel/grf/pkg/fields"
"github.com/glothriel/grf/pkg/types"
"gorm.io/datatypes"
)

type ToInternalValueDetector interface {
Expand Down Expand Up @@ -108,6 +110,31 @@ func (p *encodingTextUnmarshalerToInternalValueDetector[Model]) ToInternalValue(
return nil, fmt.Errorf("Field `%s` is not a encoding.TextUnmarshaler", fieldName)
}

type gormDataTypesJSONToInternalValueDetector[Model any] struct{}

func (p *gormDataTypesJSONToInternalValueDetector[Model]) ToInternalValue(fieldName string) (fields.InternalValueFunc, error) {
fieldSettings := getFieldSettings[Model](fieldName)
if fieldSettings.isDataTypesJSON {
return ConvertFuncToInternalValueFuncAdapter(
func(v any) (any, error) {
vAsBytes, marshalErr := json.Marshal(v)
var dtj datatypes.JSON
unmarshalErr := dtj.UnmarshalJSON(vAsBytes)
if unmarshalErr != nil || marshalErr != nil {
return nil, fmt.Errorf(
"Failed to unmarshal field `%s` from JSON: %w, %w",
fieldName,
marshalErr,
unmarshalErr,
)
}
return dtj, nil
},
), nil
}
return nil, fmt.Errorf("Field `%s` is not a encoding.TextUnmarshaler", fieldName)
}

type usingSqlNullFieldToInternalValueDetector[Model any, sqlNullType any] struct {
valueFunc func(v any) (any, error)
}
Expand Down Expand Up @@ -168,6 +195,7 @@ func DefaultToInternalValueDetector[Model any]() ToInternalValueDetector {
children: []ToInternalValueDetector{
&usingGRFParsableToInternalValueDetector[Model]{},
&isoTimeTimeToInternalValueDetector[Model]{},
&gormDataTypesJSONToInternalValueDetector[Model]{},
&fromTypeMapperToInternalValueDetector[Model]{
mapper: types.Mapper(),
modelTypeNames: FieldTypes[Model](),
Expand Down
42 changes: 42 additions & 0 deletions pkg/detectors/internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"gorm.io/datatypes"
)

type someStruct struct{}
Expand Down Expand Up @@ -242,3 +243,44 @@ func TestToInternalValue_Uint64(t *testing.T) {

testSqlNullModelsToInternalValue[uint64Model](t, float64(42), uint64(42))
}

func TestToInternalValueDataTypesJSON(t *testing.T) {
// given
type jsonModel struct {
Data datatypes.JSON `json:"data"`
}
detector := DefaultToInternalValueDetector[jsonModel]()

// when
internalValueFunc, internalValieFuncErr := detector.ToInternalValue("data")
iv, ivErr := internalValueFunc(
map[string]any{"data": map[string]any{"key": "value"}},
"data",
nil,
)

// then
assert.NoError(t, internalValieFuncErr)
assert.NoError(t, ivErr)
assert.Equal(t, datatypes.JSON(`{"key":"value"}`), iv)
}

func TestToInternalValueDataTypesJSONNotJSONSerializable(t *testing.T) {
// given
type jsonModel struct {
Data datatypes.JSON `json:"data"`
}
detector := DefaultToInternalValueDetector[jsonModel]()

// when
internalValueFunc, internalValueFuncErr := detector.ToInternalValue("data")
_, ivErr := internalValueFunc(
map[string]any{"data": make(chan int)},
"data",
nil,
)

// then
assert.NoError(t, internalValueFuncErr)
assert.Error(t, ivErr)
}
32 changes: 32 additions & 0 deletions pkg/detectors/representation.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package detectors
import (
"database/sql"
"encoding"
"encoding/json"
"fmt"
"reflect"
"time"
Expand All @@ -11,6 +12,7 @@ import (
"github.com/glothriel/grf/pkg/fields"
"github.com/glothriel/grf/pkg/models"
"github.com/glothriel/grf/pkg/types"
"gorm.io/datatypes"
)

// ToRepresentationDetector is an interface that allows to detect the representation function for a given field
Expand All @@ -26,6 +28,7 @@ func DefaultToRepresentationDetector[Model any]() ToRepresentationDetector[Model
children: []ToRepresentationDetector[Model]{
&usingGRFRepresentableToRepresentationProvider[Model]{},
&timeTimeToRepresentationProvider[Model]{},
&gormDataTypesJSONToRepresentationProvider[Model]{},
&fromTypeMapperToRepresentationProvider[Model]{
mapper: types.Mapper(),
modelTypeNames: FieldTypes[Model](),
Expand Down Expand Up @@ -213,6 +216,35 @@ func (p encodingTextMarshalerToRepresentationProvider[Model]) ToRepresentation(f
return nil, fmt.Errorf("Field `%s` is not a encoding.TextMarshaler", fieldName)
}

type gormDataTypesJSONToRepresentationProvider[Model any] struct{}

func (p gormDataTypesJSONToRepresentationProvider[Model]) ToRepresentation(fieldName string) (fields.RepresentationFunc, error) {
fieldSettings := getFieldSettings[Model](fieldName)
if fieldSettings != nil && fieldSettings.isDataTypesJSON {
return ConvertFuncToRepresentationFuncAdapter(
func(v any) (any, error) {
vAsJSON, ok := v.(datatypes.JSON)
if !ok {
return nil, fmt.Errorf("Field `%s` is not a datatypes.JSON", fieldName)
}
rawJSON, marshalJSONErr := vAsJSON.MarshalJSON()
var ret any
unmarshalErr := json.Unmarshal(rawJSON, &ret)
if unmarshalErr != nil || marshalJSONErr != nil {
return nil, fmt.Errorf(
"Failed to unmarshal field `%s` from JSON: %w, %w",
fieldName,
marshalJSONErr,
unmarshalErr,
)
}
return ret, nil
},
), nil
}
return nil, fmt.Errorf("Field `%s` is not a encoding.TextMarshaler", fieldName)
}

type chainingToRepresentationDetector[Model any] struct {
children []ToRepresentationDetector[Model]
}
Expand Down
60 changes: 60 additions & 0 deletions pkg/detectors/representation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"database/sql"
"testing"

"github.com/glothriel/grf/pkg/models"
"github.com/stretchr/testify/assert"
"gorm.io/datatypes"
)

func testSqlNullModelsToRepresentation[Model any](t *testing.T, value any, expected any) {
Expand Down Expand Up @@ -163,3 +165,61 @@ func TestToRepresentation_SQLNullBool(t *testing.T) {
Valid: false,
}, nil)
}

func TestToRepresentationGormDatatypesJSON(t *testing.T) {
// given
type gormDatatypesJSONModel struct {
Data datatypes.JSON `json:"data"`
}
detector := &gormDataTypesJSONToRepresentationProvider[gormDatatypesJSONModel]{}

// when
toRepresentation, toRepresentationErr := detector.ToRepresentation("data")
representation, representationErr := toRepresentation(models.InternalValue{
"value": datatypes.JSON([]byte(`{"key": "value"}`)),
}, "value", nil)

// then
assert.NoError(t, toRepresentationErr)
assert.NoError(t, representationErr)
assert.Equal(t, map[string]any{"key": "value"}, representation)

}

func TestToRepresentationGormDatatypesNotDatatypesJSON(t *testing.T) {
// given
type gormDatatypesJSONModel struct {
Data datatypes.JSON `json:"data"`
}
detector := &gormDataTypesJSONToRepresentationProvider[gormDatatypesJSONModel]{}

// when
toRepresentation, toRepresentationErr := detector.ToRepresentation("data")
_, representationErr := toRepresentation(models.InternalValue{
"value": "suprajs bijacz",
}, "value", nil)

// then
assert.NoError(t, toRepresentationErr)
assert.Error(t, representationErr)

}

func TestToRepresentationGormDatatypesNotValidJSON(t *testing.T) {
// given
type gormDatatypesJSONModel struct {
Data datatypes.JSON `json:"data"`
}
detector := &gormDataTypesJSONToRepresentationProvider[gormDatatypesJSONModel]{}

// when
toRepresentation, toRepresentationErr := detector.ToRepresentation("data")
_, representationErr := toRepresentation(models.InternalValue{
"value": datatypes.JSON([]byte(`{"key": "value",}`)),
}, "value", nil)

// then
assert.NoError(t, toRepresentationErr)
assert.Error(t, representationErr)

}
Loading
Loading