Skip to content

Commit 2e1e495

Browse files
committed
improvements on pg middleware's EntityController
1 parent a277a46 commit 2e1e495

File tree

4 files changed

+151
-11
lines changed

4 files changed

+151
-11
lines changed

pg/_examples/controller/main.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,15 @@ func main() {
4747
app.Logger().SetLevel("debug")
4848

4949
pgMiddleware := newPG()
50+
defer pgMiddleware.Close()
5051

51-
customerController := pg.NewEntityController[Customer](pgMiddleware)
52+
customerController := pg.NewEntityController[Customer](pgMiddleware) // .WithoutSchemaRoute()
5253
app.PartyConfigure("/api/customer", customerController)
5354

55+
// GET /api/customer/schema
56+
// GET /api/customer/{id}
57+
// POST /api/customer
58+
// PUT /api/customer
59+
// DELETE /api/customer/{id}
5460
app.Listen(":8080")
5561
}

pg/entity_controller.go

Lines changed: 137 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ package pg
22

33
import (
44
"fmt"
5+
"reflect"
6+
"strings"
57

68
"github.com/kataras/iris/v12"
79
"github.com/kataras/iris/v12/x/errors"
10+
"github.com/kataras/iris/v12/x/jsonx"
811

912
"github.com/kataras/pg"
1013
"github.com/kataras/pg/desc"
@@ -23,25 +26,32 @@ import (
2326
// - DELETE /{id} - deletes an entity by ID.
2427
// The {id} parameter is the entity ID. It can be a string, int, uint, uuid, etc.
2528
type EntityController[T any] struct {
29+
iris.Singleton
30+
2631
repository *pg.Repository[T]
2732

2833
tableName string
2934
primaryKeyType desc.DataType
3035

36+
disableSchemaRoute bool
37+
3138
// GetID returns the entity ID for GET/{id} and DELETE/{id} paths from the request Context.
3239
GetID func(ctx iris.Context) any
3340

3441
// ErrorHandler defaults to the PG's error handler. It can be customized for this controller.
3542
// Setting this to nil will panic the application on the first error.
3643
ErrorHandler func(ctx iris.Context, err error) bool
44+
45+
// AfterPayloadRead is called after the payload is read.
46+
// It can be used to validate the payload or set default fields based on the request Context.
47+
AfterPayloadRead func(ctx iris.Context, payload T) (T, bool)
3748
}
3849

3950
// NewEntityController returns a new EntityController[T].
4051
// The T is the entity type (e.g. a custom type, Customer).
4152
//
4253
// Read the type's documentation for more information.
4354
func NewEntityController[T any](middleware *PG) *EntityController[T] {
44-
4555
repo := pg.NewRepository[T](middleware.GetDB())
4656
errorHandler := middleware.opts.handleError
4757

@@ -51,16 +61,21 @@ func NewEntityController[T any](middleware *PG) *EntityController[T] {
5161
panic(fmt.Sprintf("pg: entity %s does not have a primary key", td.Name))
5262
}
5363

54-
return &EntityController[T]{
64+
controller := &EntityController[T]{
5565
repository: repo,
5666
tableName: td.Name,
5767
primaryKeyType: primaryKey.Type,
5868
ErrorHandler: errorHandler,
5969
}
70+
71+
return controller
6072
}
6173

62-
// Singleton returns true as this controller is a singleton.
63-
func (c *EntityController[T]) Singleton() bool { return true }
74+
// WithoutSchemaRoute disables the GET /schema route.
75+
func (c *EntityController[T]) WithoutSchemaRoute() *EntityController[T] {
76+
c.disableSchemaRoute = true
77+
return c
78+
}
6479

6580
// Configure registers the controller's routes.
6681
// It is called automatically by the Iris API Builder when registered to the Iris Application.
@@ -92,6 +107,11 @@ func (c *EntityController[T]) Configure(r iris.Party) {
92107
}
93108
}
94109

110+
if !c.disableSchemaRoute {
111+
jsonSchema := newJSONSchema[T](c.repository.Table())
112+
r.Get("/schema", c.getSchema(jsonSchema))
113+
}
114+
95115
r.Post("/", c.create)
96116
r.Put("/", c.update)
97117
r.Get(idParam, c.get)
@@ -112,13 +132,125 @@ func (c *EntityController[T]) readPayload(ctx iris.Context) (T, bool) {
112132
var payload T
113133
err := ctx.ReadJSON(&payload)
114134
if err != nil {
115-
errors.InvalidArgument.Details(ctx, "unable to parse body", err.Error())
135+
if vErrs, ok := errors.AsValidationErrors(err); ok {
136+
errors.InvalidArgument.Data(ctx, "validation failure", vErrs)
137+
} else {
138+
errors.InvalidArgument.Details(ctx, "unable to parse body", err.Error())
139+
}
116140
return payload, false
117141
}
118142

143+
if c.AfterPayloadRead != nil {
144+
return c.AfterPayloadRead(ctx, payload)
145+
}
146+
119147
return payload, true
120148
}
121149

150+
type jsonSchema[T any] struct {
151+
Description string `json:"description,omitempty"`
152+
Types []jsonSchemaFieldType `json:"types,omitempty"`
153+
Fields []jsonSchemaField `json:"fields"`
154+
}
155+
156+
type jsonSchemaFieldType struct {
157+
Type string `json:"type"`
158+
Examples []string `json:"examples,omitempty"`
159+
}
160+
161+
type jsonSchemaField struct {
162+
Name string `json:"name"`
163+
Description string `json:"description,omitempty"`
164+
Type string `json:"type"`
165+
DataType string `json:"data_type"`
166+
Required bool `json:"required"`
167+
}
168+
169+
func getJSONTag(v interface{}, fieldIndex []int) (string, bool) {
170+
t := reflect.TypeOf(v)
171+
f := t.FieldByIndex(fieldIndex)
172+
jsonTag := f.Tag.Get("json")
173+
if jsonTag == "" {
174+
return "", false
175+
}
176+
177+
return strings.Split(jsonTag, ",")[0], true
178+
}
179+
180+
func newJSONSchema[T any](td *desc.Table) *jsonSchema[T] {
181+
var fieldTypes []jsonSchemaFieldType
182+
seenFieldTypes := make(map[reflect.Type]struct{})
183+
184+
var t T
185+
fields := make([]jsonSchemaField, 0, len(td.Columns))
186+
for _, col := range td.Columns {
187+
fieldName, ok := getJSONTag(t, col.FieldIndex)
188+
if !ok {
189+
fieldName = col.Name
190+
}
191+
192+
// Get the field type examples.
193+
if _, seen := seenFieldTypes[col.FieldType]; !seen {
194+
seenFieldTypes[col.FieldType] = struct{}{}
195+
196+
colValue := reflect.New(col.FieldType).Interface()
197+
if exampler, ok := colValue.(jsonx.Exampler); ok {
198+
exampleValues := exampler.ListExamples()
199+
fieldTypes = append(fieldTypes, jsonSchemaFieldType{
200+
Type: col.FieldType.String(),
201+
Examples: exampleValues,
202+
})
203+
}
204+
205+
/*
206+
if m, ok := col.FieldType.MethodByName("Examples"); ok {
207+
var returnValues []reflect.Value
208+
if col.FieldType.Kind() == reflect.Ptr {
209+
returnValues = m.Func.Call([]reflect.Value{newColFieldValue})
210+
} else {
211+
returnValues = m.Func.Call([]reflect.Value{newColFieldValue.Elem()})
212+
}
213+
214+
if len(returnValues) > 0 && returnValues[0].CanInterface() {
215+
v := returnValues[0].Interface()
216+
if v != nil {
217+
if exampleValues, ok := v.([]string); ok {
218+
fieldTypes = append(fieldTypes, jsonSchemaFieldType{
219+
Type: col.FieldType.String(),
220+
Examples: exampleValues,
221+
})
222+
}
223+
}
224+
}
225+
}
226+
*/
227+
}
228+
229+
field := jsonSchemaField{
230+
// Here we want the json tag name, not the column name.
231+
Name: fieldName,
232+
Description: col.Description,
233+
Type: col.FieldType.String(),
234+
DataType: col.Type.String(),
235+
Required: !col.Nullable,
236+
}
237+
238+
fields = append(fields, field)
239+
}
240+
241+
return &jsonSchema[T]{
242+
Description: td.Description,
243+
Types: fieldTypes,
244+
Fields: fields,
245+
}
246+
}
247+
248+
func (c *EntityController[T]) getSchema(s *jsonSchema[T]) iris.Handler {
249+
return func(ctx iris.Context) {
250+
ctx.JSON(s)
251+
}
252+
}
253+
122254
type idPayload struct {
123255
ID any `json:"id"`
124256
}

pg/go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ module github.com/iris-contrib/middleware/pg
22

33
go 1.21
44

5-
replace github.com/kataras/iris/v12 => C:\kataras\github\iris
5+
// replace github.com/kataras/iris/v12 => C:\kataras\github\iris
66

77
require (
88
github.com/kataras/golog v0.1.9
9-
github.com/kataras/iris/v12 v12.2.5
9+
github.com/kataras/iris/v12 v12.2.6-0.20230825084054-21fa56720f5c
1010
github.com/kataras/pg v1.0.5
1111
github.com/kataras/pgx-golog v0.0.1
1212
)
@@ -24,7 +24,7 @@ require (
2424
github.com/gertd/go-pluralize v0.2.1 // indirect
2525
github.com/golang/snappy v0.0.4 // indirect
2626
github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12 // indirect
27-
github.com/google/uuid v1.3.0 // indirect
27+
github.com/google/uuid v1.3.1 // indirect
2828
github.com/gorilla/css v1.0.0 // indirect
2929
github.com/iris-contrib/schema v0.0.6 // indirect
3030
github.com/jackc/pgpassfile v1.0.0 // indirect

pg/go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
3838
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
3939
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
4040
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
41-
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
42-
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
41+
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
42+
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
4343
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
4444
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
4545
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
@@ -64,6 +64,8 @@ github.com/kataras/blocks v0.0.7 h1:cF3RDY/vxnSRezc7vLFlQFTYXG/yAr1o7WImJuZbzC4=
6464
github.com/kataras/blocks v0.0.7/go.mod h1:UJIU97CluDo0f+zEjbnbkeMRlvYORtmc1304EeyXf4I=
6565
github.com/kataras/golog v0.1.9 h1:vLvSDpP7kihFGKFAvBSofYo7qZNULYSHOH2D7rPTKJk=
6666
github.com/kataras/golog v0.1.9/go.mod h1:jlpk/bOaYCyqDqH18pgDHdaJab72yBE6i0O3s30hpWY=
67+
github.com/kataras/iris/v12 v12.2.6-0.20230825084054-21fa56720f5c h1:IY9llS6ovQbON4igvdqv+oLhnKwhi4Cyg2vGSLYCF+o=
68+
github.com/kataras/iris/v12 v12.2.6-0.20230825084054-21fa56720f5c/go.mod h1:wIyyHF5GFl9Guy6NJdQREPCmst337NFWIWZHlxUtDCY=
6769
github.com/kataras/pg v1.0.5 h1:4XaqZT7LkFsaGzIQds3kDqfZwOB61yKRMi9Z2gQDlR8=
6870
github.com/kataras/pg v1.0.5/go.mod h1:BLSUSkLSf+/zJ+mFffB/YQUUWbH4TSCqZTlAFHpHLLA=
6971
github.com/kataras/pgx-golog v0.0.1 h1:e8bankbEM/2rKLgtb6wiiB0ze5nY+6cx3wmr1bj+KEI=

0 commit comments

Comments
 (0)