@@ -2,9 +2,12 @@ package pg
2
2
3
3
import (
4
4
"fmt"
5
+ "reflect"
6
+ "strings"
5
7
6
8
"github.com/kataras/iris/v12"
7
9
"github.com/kataras/iris/v12/x/errors"
10
+ "github.com/kataras/iris/v12/x/jsonx"
8
11
9
12
"github.com/kataras/pg"
10
13
"github.com/kataras/pg/desc"
@@ -23,25 +26,32 @@ import (
23
26
// - DELETE /{id} - deletes an entity by ID.
24
27
// The {id} parameter is the entity ID. It can be a string, int, uint, uuid, etc.
25
28
type EntityController [T any ] struct {
29
+ iris.Singleton
30
+
26
31
repository * pg.Repository [T ]
27
32
28
33
tableName string
29
34
primaryKeyType desc.DataType
30
35
36
+ disableSchemaRoute bool
37
+
31
38
// GetID returns the entity ID for GET/{id} and DELETE/{id} paths from the request Context.
32
39
GetID func (ctx iris.Context ) any
33
40
34
41
// ErrorHandler defaults to the PG's error handler. It can be customized for this controller.
35
42
// Setting this to nil will panic the application on the first error.
36
43
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 )
37
48
}
38
49
39
50
// NewEntityController returns a new EntityController[T].
40
51
// The T is the entity type (e.g. a custom type, Customer).
41
52
//
42
53
// Read the type's documentation for more information.
43
54
func NewEntityController [T any ](middleware * PG ) * EntityController [T ] {
44
-
45
55
repo := pg.NewRepository [T ](middleware .GetDB ())
46
56
errorHandler := middleware .opts .handleError
47
57
@@ -51,16 +61,21 @@ func NewEntityController[T any](middleware *PG) *EntityController[T] {
51
61
panic (fmt .Sprintf ("pg: entity %s does not have a primary key" , td .Name ))
52
62
}
53
63
54
- return & EntityController [T ]{
64
+ controller := & EntityController [T ]{
55
65
repository : repo ,
56
66
tableName : td .Name ,
57
67
primaryKeyType : primaryKey .Type ,
58
68
ErrorHandler : errorHandler ,
59
69
}
70
+
71
+ return controller
60
72
}
61
73
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
+ }
64
79
65
80
// Configure registers the controller's routes.
66
81
// 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) {
92
107
}
93
108
}
94
109
110
+ if ! c .disableSchemaRoute {
111
+ jsonSchema := newJSONSchema [T ](c .repository .Table ())
112
+ r .Get ("/schema" , c .getSchema (jsonSchema ))
113
+ }
114
+
95
115
r .Post ("/" , c .create )
96
116
r .Put ("/" , c .update )
97
117
r .Get (idParam , c .get )
@@ -112,13 +132,125 @@ func (c *EntityController[T]) readPayload(ctx iris.Context) (T, bool) {
112
132
var payload T
113
133
err := ctx .ReadJSON (& payload )
114
134
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
+ }
116
140
return payload , false
117
141
}
118
142
143
+ if c .AfterPayloadRead != nil {
144
+ return c .AfterPayloadRead (ctx , payload )
145
+ }
146
+
119
147
return payload , true
120
148
}
121
149
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
+
122
254
type idPayload struct {
123
255
ID any `json:"id"`
124
256
}
0 commit comments