Skip to content

Commit 4f53d0e

Browse files
authored
Merge pull request #11 from Philiphil/s2
serializer second major update, 1.1.0
2 parents 31ffe21 + 259eec6 commit 4f53d0e

34 files changed

Lines changed: 648 additions & 368 deletions

README.md

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -252,13 +252,13 @@ Override settings for specific operations:
252252
routes := route.DefaultApiRoutes()
253253

254254
getConfig := configuration.DefaultRouteConfiguration().
255-
SerializationGroups("read", "public").
255+
InputSerializationGroups("read", "public").
256256
ItemPerPage(100)
257257

258258
routes.Get.Configure(getConfig)
259259

260260
postConfig := configuration.DefaultRouteConfiguration().
261-
SerializationGroups("write")
261+
InputSerializationGroups("write")
262262

263263
routes.Post.Configure(postConfig)
264264

@@ -457,8 +457,8 @@ go test ./test/router/...
457457
## Roadmap
458458

459459
### TODO/ IDEAS
460+
- [ ] Add random configuration to clarify behavior, suggest best practices and allow flexibility (right now clarifying backup configuration)
460461
- [ ] Filtering implementation
461-
- [ ] Groups override parameter
462462
- [ ] UUID compatibility for entity.ID
463463
- [ ] Force lowercase option for JSON keys
464464
- [ ] Automatic Redis caching integration in router
@@ -474,16 +474,6 @@ go test ./test/router/...
474474
- [ ] Graphql like PageInfo object after, before, first, last, pageof
475475

476476

477-
### Completed
478-
- [x] MongoDB repository implementation
479-
- [x] Redis caching library (manual usage)
480-
- [x] XML serialization
481-
- [x] CSV serialization
482-
- [x] MessagePack support
483-
- [x] Subresource routing
484-
- [x] Batch operations
485-
- [x] JSON-LD with Hydra collections
486-
487477
## License
488478

489479
MIT License - see [LICENSE](LICENSE) file for details

configuration/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ The Configuration package provides the default behavior for ApiRouter by definin
77
## Purpose
88

99
When processing requests (e.g., `GET /api/item`), ApiRouter determines how to respond (e.g., with `GetList`) based on configuration settings for:
10+
- router global behavior
1011
- Sorting
1112
- Pagination
1213
- Filtering

configuration/configuration.go

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@ const (
2020
// Value in seconds. Default: 0 (no caching)
2121
NetworkCachingPolicyType
2222

23-
// SerializationGroupsType defines which field groups to include in serialization
24-
// Used with struct tags: `groups:"read,write"`
25-
SerializationGroupsType
23+
// InputSerializationGroupsType defines which field groups to include in serialization
24+
// Used with struct tags: `groups:"read,read_public,read_plus"`
25+
InputSerializationGroupsType
26+
27+
// OutputSerializationGroupsType defines which field groups to include in output serialization
28+
OutputSerializationGroupsType
2629

2730
// MaxItemPerPageType sets the maximum allowed items per page (default: 1000)
2831
// Prevents clients from requesting too many items at once
@@ -70,12 +73,24 @@ const (
7073
// Whitelist to prevent sorting on sensitive or non-indexed fields
7174
SortableFieldsType
7275

73-
GroupOverwriteClientControlType // Allows clients to overwrite serialization groups
74-
GroupOverwriteParameterNameType // Query parameter name for overwriting serialization groups
76+
OutputSerializationGroupOverwriteClientControlType // Allows clients to overwrite serialization groups
77+
OutputSerializationGroupOverwriteParameterNameType // Query parameter name for overwriting serialization groups
7578

7679
// Unimplemented configuration types - reserved for future use
77-
BatchLimitType // Will limit the number of items in batch operations
78-
TypeEnabledType // Will enable/disable specific route types
80+
81+
// Whether write routes default to read output serialization
82+
//seems weird at first but what should be the output of POST if POST has no specific output serialization groups configured?
83+
//logicaly it should be the same as GET (read)
84+
//this set as true allows this behavior for POST, PUT, PATCH routes
85+
WriteRouteOutputShouldDefaultToReadOutputType
86+
87+
// Whether batch routes default to single entity route configuration
88+
// Batch route configurations (e.g., BatchGet, BatchPatch) can be configured dirrectly or otherwise fallback to router wide configuration
89+
// this will allow then to fallback to single entity route configuration
90+
BatchRouteConfigurationDefaultToSingleRouteConfigurationType
91+
92+
BatchLimitType // Will limit the number of items in batch operations ...
93+
FormatEnabledType // Will enable/disable specific format
7994
DefaultFilteringType // Will add default filters to queries
8095
InMemoryCachingPolicyType // Will configure in-memory caching
8196
)
@@ -130,9 +145,13 @@ func RouteName(name string) Configuration {
130145
//
131146
// Example:
132147
//
133-
// configuration.SerializationGroups("read", "public")
134-
func SerializationGroups(groups ...string) Configuration {
135-
return Configuration{Type: SerializationGroupsType, Values: groups}
148+
// configuration.InputSerializationGroups("read", "public")
149+
func InputSerializationGroups(groups ...string) Configuration {
150+
return Configuration{Type: InputSerializationGroupsType, Values: groups}
151+
}
152+
153+
func OutputSerializationGroups(groups ...string) Configuration {
154+
return Configuration{Type: OutputSerializationGroupsType, Values: groups}
136155
}
137156

138157
// MaxItemPerPage sets the maximum allowed items per page. Default is 1000.
@@ -257,3 +276,19 @@ func SortingClientControl(enabled bool) Configuration {
257276
func SortableFields(fields ...string) Configuration {
258277
return Configuration{Type: SortableFieldsType, Values: fields}
259278
}
279+
280+
func OutputSerializationGroupOverwriteClientControl(enabled bool) Configuration {
281+
return Configuration{Type: OutputSerializationGroupOverwriteClientControlType, Values: []string{strconv.FormatBool(enabled)}}
282+
}
283+
284+
func OutputSerializationGroupOverwriteParameterName(name string) Configuration {
285+
return Configuration{Type: OutputSerializationGroupOverwriteParameterNameType, Values: []string{name}}
286+
}
287+
288+
func WriteRouteOutputShouldDefaultToReadOutput(enabled bool) Configuration {
289+
return Configuration{Type: WriteRouteOutputShouldDefaultToReadOutputType, Values: []string{strconv.FormatBool(enabled)}}
290+
}
291+
292+
func BatchRouteConfigurationDefaultToSingleRouteConfiguration(enabled bool) Configuration {
293+
return Configuration{Type: BatchRouteConfigurationDefaultToSingleRouteConfigurationType, Values: []string{strconv.FormatBool(enabled)}}
294+
}

configuration/default_configuration.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ package configuration
33
// DefaultConfiguration returns the default configuration map used by an ApiRouter.
44
func DefaultConfiguration() map[ConfigurationType]Configuration {
55
return map[ConfigurationType]Configuration{
6-
RoutePrefixType: RoutePrefix("api"),
7-
NetworkCachingPolicyType: NetworkCachingPolicy(0),
8-
SerializationGroupsType: SerializationGroups(),
6+
RoutePrefixType: RoutePrefix("api"),
7+
NetworkCachingPolicyType: NetworkCachingPolicy(0),
8+
9+
InputSerializationGroupsType: InputSerializationGroups(),
10+
OutputSerializationGroupsType: OutputSerializationGroups(),
11+
912
PaginationType: Pagination(true),
1013
PageParameterNameType: PageParameterName("page"),
1114
PaginationClientControlType: PaginationClientControl(false),
@@ -19,5 +22,12 @@ func DefaultConfiguration() map[ConfigurationType]Configuration {
1922
SortingType: Sorting(map[string]string{"id": "asc"}),
2023
SortingParameterNameType: SortingParameterName("sort"),
2124
SortableFieldsType: SortableFields("id"),
25+
26+
OutputSerializationGroupOverwriteClientControlType: OutputSerializationGroupOverwriteClientControl(false),
27+
OutputSerializationGroupOverwriteParameterNameType: OutputSerializationGroupOverwriteParameterName("groupOverwrite"),
28+
29+
//not implemented yet
30+
WriteRouteOutputShouldDefaultToReadOutputType: WriteRouteOutputShouldDefaultToReadOutput(true),
31+
BatchRouteConfigurationDefaultToSingleRouteConfigurationType: BatchRouteConfigurationDefaultToSingleRouteConfiguration(true),
2232
}
2333
}

example/custom_serialization_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func (u User) SetId(id any) entity.Entity {
3131
u.Id = entity.CastId(id)
3232
return u
3333
}
34-
func (u User) ToEntity() User { return u }
34+
func (u User) ToEntity() User { return u }
3535
func (u User) FromEntity(e User) any { return e }
3636

3737
func getSerializationDB() *gorm.DB {
@@ -57,7 +57,7 @@ func TestCustomSerialization(t *testing.T) {
5757
userRouter := router.NewApiRouter(
5858
*orm.NewORM(gormrepository.NewRepository[User](db)),
5959
routes,
60-
configuration.SerializationGroups("read", "public"),
60+
configuration.InputSerializationGroups("read", "public"),
6161
)
6262

6363
userRouter.AllowRoutes(r)

example/router_conf_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func TestRouterConfiguration(t *testing.T) {
3535
route.DefaultApiRoutes(),
3636
configuration.RoutePrefix("api"),
3737
configuration.NetworkCachingPolicy(0),
38-
configuration.SerializationGroups(),
38+
configuration.InputSerializationGroups(),
3939
configuration.Pagination(true),
4040
configuration.PaginationClientControl(false),
4141
configuration.ItemPerPage(100),
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package example_test
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
10+
"github.com/gin-gonic/gin"
11+
"github.com/philiphil/restman/configuration"
12+
"github.com/philiphil/restman/orm"
13+
"github.com/philiphil/restman/orm/entity"
14+
"github.com/philiphil/restman/orm/gormrepository"
15+
"github.com/philiphil/restman/route"
16+
"github.com/philiphil/restman/router"
17+
"gorm.io/driver/sqlite"
18+
"gorm.io/gorm"
19+
"gorm.io/gorm/logger"
20+
)
21+
22+
//Serialization groups are an easy way to control which fields are included in API responses and requests based on context.
23+
// In this example, we define "read" and "write" groups to manage field visibility during GET and POST/PUT operations respectively.
24+
25+
// With SerializationGroups configuration , as with any configuration we can set router-wide defaults that can be overridden on a operation level
26+
// we also provide client control over group overwriting via query parameters.
27+
28+
type SerialProduct struct {
29+
entity.BaseEntity
30+
Name string `json:"name" groups:"read,write"`
31+
Price float64 `json:"price" groups:"write"`
32+
InternalSKU string `json:"internal_sku" groups:"read"` //should be never editable
33+
CostPrice float64 `json:"cost_price" groups:"private_stuff"` //should be never visible via api
34+
}
35+
36+
func (p SerialProduct) GetId() entity.ID { return p.Id }
37+
func (p SerialProduct) SetId(id any) entity.Entity {
38+
p.Id = entity.CastId(id)
39+
return p
40+
}
41+
func (p SerialProduct) ToEntity() SerialProduct { return p }
42+
func (p SerialProduct) FromEntity(e SerialProduct) any { return e }
43+
44+
func getSerialProductDB() *gorm.DB {
45+
db, err := gorm.Open(sqlite.Open("file:serial_product_test?mode=memory&cache=shared&_fk=1"), &gorm.Config{
46+
Logger: logger.Default.LogMode(logger.Silent),
47+
CreateBatchSize: 1000,
48+
})
49+
if err != nil {
50+
panic(err)
51+
}
52+
return db
53+
}
54+
55+
func TestSerializationGroupsReadWrite(t *testing.T) {
56+
db := getSerialProductDB()
57+
db.AutoMigrate(&SerialProduct{})
58+
59+
r := gin.New()
60+
r.Use(gin.Recovery())
61+
62+
routes := route.DefaultApiRoutes()
63+
routes[route.Post].Configuration[configuration.InputSerializationGroupsType] = configuration.InputSerializationGroups("write")
64+
65+
productRouter := router.NewApiRouter(
66+
*orm.NewORM(gormrepository.NewRepository[SerialProduct](db)),
67+
routes,
68+
configuration.OutputSerializationGroups("read"), //default router-wide serialization group should be "read"
69+
)
70+
71+
productRouter.AllowRoutes(r)
72+
73+
postData := SerialProduct{
74+
Name: "Laptop",
75+
Price: 999.99,
76+
CostPrice: 700.00,
77+
InternalSKU: "SKU-IGNORED",
78+
}
79+
jsonData, _ := json.Marshal(postData)
80+
81+
w := httptest.NewRecorder()
82+
req, _ := http.NewRequest("POST", "/api/serial_product", bytes.NewBuffer(jsonData))
83+
req.Header.Set("Content-Type", "application/json")
84+
r.ServeHTTP(w, req)
85+
86+
if w.Code != http.StatusCreated {
87+
t.Errorf("Expected status 201, got %d. Body: %s", w.Code, w.Body.String())
88+
}
89+
90+
var createdProduct SerialProduct
91+
db.First(&createdProduct, 1)
92+
93+
if createdProduct.Name != "Laptop" {
94+
t.Errorf("Name should have been written, got: %s", createdProduct.Name)
95+
}
96+
97+
if createdProduct.Price != 999.99 {
98+
t.Errorf("Price should have been written to DB, got: %f", createdProduct.Price)
99+
}
100+
101+
if createdProduct.InternalSKU == "SKU-IGNORED" {
102+
t.Error("InternalSKU should not have been written (not in write group)")
103+
}
104+
105+
if createdProduct.CostPrice == 700.00 {
106+
t.Error("CostPrice should not have been written (not in write group)")
107+
}
108+
109+
w = httptest.NewRecorder()
110+
req, _ = http.NewRequest("GET", "/api/serial_product/1", nil)
111+
r.ServeHTTP(w, req)
112+
113+
if w.Code != http.StatusOK {
114+
t.Errorf("Expected status 200, got %d", w.Code)
115+
}
116+
117+
var response SerialProduct
118+
json.Unmarshal(w.Body.Bytes(), &response)
119+
120+
if response.Name != "Laptop" {
121+
t.Error("Name should be visible in GET response (read group)")
122+
}
123+
124+
if response.Price != 0 {
125+
t.Errorf("Price should NOT be visible in GET response (write group only), got: %f", response.Price)
126+
}
127+
128+
if response.InternalSKU != "" {
129+
t.Errorf("InternalSKU should be empty in GET (read group but never written), got: %s", response.InternalSKU)
130+
}
131+
132+
if response.CostPrice != 0 {
133+
t.Errorf("CostPrice should be zero in GET (private_stuff group not included), got: %f", response.CostPrice)
134+
}
135+
}

go.mod

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ go 1.24.0
55
require (
66
github.com/gin-gonic/gin v1.11.0
77
github.com/go-redis/redismock/v9 v9.2.0
8-
github.com/redis/go-redis/v9 v9.14.0
8+
github.com/redis/go-redis/v9 v9.16.0
9+
github.com/vmihailenco/msgpack/v5 v5.4.1
910
go.mongodb.org/mongo-driver v1.17.4
1011
gorm.io/driver/sqlite v1.6.0
1112
gorm.io/gorm v1.31.0
@@ -16,22 +17,21 @@ require (
1617
github.com/cespare/xxhash/v2 v2.3.0 // indirect
1718
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
1819
github.com/goccy/go-yaml v1.18.0 // indirect
19-
github.com/golang/snappy v0.0.4 // indirect
20-
github.com/klauspost/compress v1.16.7 // indirect
20+
github.com/golang/snappy v1.0.0 // indirect
21+
github.com/klauspost/compress v1.18.1 // indirect
2122
github.com/montanaflynn/stats v0.7.1 // indirect
2223
github.com/onsi/gomega v1.38.2 // indirect
2324
github.com/quic-go/qpack v0.5.1 // indirect
24-
github.com/quic-go/quic-go v0.54.0 // indirect
25-
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
25+
github.com/quic-go/quic-go v0.55.0 // indirect
2626
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
2727
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
2828
github.com/xdg-go/scram v1.1.2 // indirect
2929
github.com/xdg-go/stringprep v1.0.4 // indirect
3030
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
31-
go.uber.org/mock v0.5.0 // indirect
32-
golang.org/x/mod v0.28.0 // indirect
31+
go.uber.org/mock v0.6.0 // indirect
32+
golang.org/x/mod v0.29.0 // indirect
3333
golang.org/x/sync v0.17.0 // indirect
34-
golang.org/x/tools v0.37.0 // indirect
34+
golang.org/x/tools v0.38.0 // indirect
3535
)
3636

3737
require (
@@ -42,7 +42,7 @@ require (
4242
github.com/gin-contrib/sse v1.1.0 // indirect
4343
github.com/go-playground/locales v0.14.1 // indirect
4444
github.com/go-playground/universal-translator v0.18.1 // indirect
45-
github.com/go-playground/validator/v10 v10.27.0 // indirect
45+
github.com/go-playground/validator/v10 v10.28.0 // indirect
4646
github.com/goccy/go-json v0.10.5 // indirect
4747
github.com/jinzhu/inflection v1.0.0 // indirect
4848
github.com/jinzhu/now v1.1.5 // indirect
@@ -56,11 +56,11 @@ require (
5656
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
5757
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
5858
github.com/ugorji/go/codec v1.3.0 // indirect
59-
golang.org/x/arch v0.21.0 // indirect
60-
golang.org/x/crypto v0.42.0 // indirect
61-
golang.org/x/exp v0.0.0-20250911091902-df9299821621
62-
golang.org/x/net v0.44.0 // indirect
63-
golang.org/x/sys v0.36.0 // indirect
64-
golang.org/x/text v0.29.0 // indirect
65-
google.golang.org/protobuf v1.36.9 // indirect
59+
golang.org/x/arch v0.22.0 // indirect
60+
golang.org/x/crypto v0.43.0 // indirect
61+
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
62+
golang.org/x/net v0.46.0 // indirect
63+
golang.org/x/sys v0.37.0 // indirect
64+
golang.org/x/text v0.30.0 // indirect
65+
google.golang.org/protobuf v1.36.10 // indirect
6666
)

0 commit comments

Comments
 (0)