From ca13064aa18498f0036774c1ff030d455fa492f8 Mon Sep 17 00:00:00 2001
From: "popov.dmitriy" <dp@sessia.dev>
Date: Thu, 15 May 2025 18:19:26 +0300
Subject: [PATCH 1/3] Fix filter

---
 request/pagination.go      | 21 ++++++++++
 request/pagination_test.go | 80 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 101 insertions(+)
 create mode 100644 request/pagination.go
 create mode 100644 request/pagination_test.go

diff --git a/request/pagination.go b/request/pagination.go
new file mode 100644
index 0000000..fccf52d
--- /dev/null
+++ b/request/pagination.go
@@ -0,0 +1,21 @@
+package request
+
+import "github.com/go-playground/validator/v10"
+
+type Pagination struct {
+	Page     int `query:"page" validate:"min=1"`
+	PageSize int `query:"page_size" validate:"min=1,max=100"`
+}
+
+func (p *Pagination) Validate() error {
+	if p.Page == 0 {
+		p.Page = 1
+	}
+
+	if p.PageSize == 0 {
+		p.PageSize = 10
+	}
+
+	validate := validator.New()
+	return validate.Struct(p)
+}
diff --git a/request/pagination_test.go b/request/pagination_test.go
new file mode 100644
index 0000000..68e1fee
--- /dev/null
+++ b/request/pagination_test.go
@@ -0,0 +1,80 @@
+package request
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestPagination_Validate(t *testing.T) {
+	tests := []struct {
+		name          string
+		input         Pagination
+		expectedPage  int
+		expectedSize  int
+		expectError   bool
+		errorContains string
+	}{
+		{
+			name:         "default values set when zero",
+			input:        Pagination{Page: 0, PageSize: 0},
+			expectedPage: 1,
+			expectedSize: 10,
+			expectError:  false,
+		},
+		{
+			name:         "valid input",
+			input:        Pagination{Page: 2, PageSize: 50},
+			expectedPage: 2,
+			expectedSize: 50,
+			expectError:  false,
+		},
+		{
+			name:         "page less than minimum",
+			input:        Pagination{Page: 0, PageSize: 10},
+			expectedPage: 1, // default set
+			expectedSize: 10,
+			expectError:  false,
+		},
+		{
+			name:         "page size less than minimum",
+			input:        Pagination{Page: 1, PageSize: 0},
+			expectedPage: 1,
+			expectedSize: 10, // default set
+			expectError:  false,
+		},
+		{
+			name:          "page size too large",
+			input:         Pagination{Page: 1, PageSize: 101},
+			expectedPage:  1,
+			expectedSize:  101,
+			expectError:   true,
+			errorContains: "PageSize",
+		},
+		{
+			name:          "page less than 1 after default",
+			input:         Pagination{Page: -1, PageSize: 10},
+			expectedPage:  -1,
+			expectedSize:  10,
+			expectError:   true,
+			errorContains: "Page",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.input.Validate()
+			assert.Equal(t, tt.expectedPage, tt.input.Page)
+			assert.Equal(t, tt.expectedSize, tt.input.PageSize)
+
+			if tt.expectError {
+				assert.Error(t, err)
+				if err != nil {
+					assert.Contains(t, err.Error(), tt.errorContains)
+				}
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
-- 
GitLab


From 00f1ebba08ed02dbecc321499f783d03fe236910 Mon Sep 17 00:00:00 2001
From: "popov.dmitriy" <dp@sessia.dev>
Date: Thu, 15 May 2025 18:19:41 +0300
Subject: [PATCH 2/3] Add pagination

---
 go.mod                 |  8 +++++
 go.sum                 | 18 ++++++++++
 request/filter.go      | 79 ++++++----------------------------------
 request/filter_test.go | 82 ++++++++++++++++++------------------------
 4 files changed, 71 insertions(+), 116 deletions(-)

diff --git a/go.mod b/go.mod
index c3af6e8..a5fdb90 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module gitlab.sessia.com/sdk/api-response-request
 go 1.23.3
 
 require (
+	github.com/go-playground/validator/v10 v10.26.0
 	github.com/gofiber/fiber/v2 v2.52.6
 	github.com/stretchr/testify v1.10.0
 )
@@ -10,8 +11,12 @@ require (
 require (
 	github.com/andybalholm/brotli v1.1.1 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/gabriel-vasile/mimetype v1.4.8 // indirect
+	github.com/go-playground/locales v0.14.1 // indirect
+	github.com/go-playground/universal-translator v0.18.1 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/klauspost/compress v1.18.0 // indirect
+	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/mattn/go-colorable v0.1.14 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mattn/go-runewidth v0.0.16 // indirect
@@ -19,6 +24,9 @@ require (
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
 	github.com/valyala/fasthttp v1.59.0 // indirect
+	golang.org/x/crypto v0.33.0 // indirect
+	golang.org/x/net v0.35.0 // indirect
 	golang.org/x/sys v0.30.0 // indirect
+	golang.org/x/text v0.22.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index adee732..f66153b 100644
--- a/go.sum
+++ b/go.sum
@@ -2,12 +2,24 @@ github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7X
 github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
+github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
+github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
 github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
 github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
 github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
 github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
 github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -27,9 +39,15 @@ github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDp
 github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
 github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
 github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
+golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
+golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
+golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
+golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
 golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
+golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/request/filter.go b/request/filter.go
index 2fbdcc4..bed0367 100644
--- a/request/filter.go
+++ b/request/filter.go
@@ -1,95 +1,38 @@
 package request
 
 import (
-	"fmt"
 	"net/url"
 	"strings"
-	"time"
 )
 
-type FilterOperation interface {
-	Field() string
-	Operation() string
-}
-
-type SimpleFilter struct {
-	FieldName string
-	Op        string
+type FilterOperation struct {
+	Field     string
+	Operation string
 	Value     string
 }
 
-func (f SimpleFilter) Field() string     { return f.FieldName }
-func (f SimpleFilter) Operation() string { return f.Op }
-
-type RangeFilter struct {
-	FieldName string
-	Range     DateRange
-}
-
-func (r RangeFilter) Field() string     { return r.FieldName }
-func (r RangeFilter) Operation() string { return "range" }
-
-type DateRange struct {
-	Begin *time.Time
-	End   *time.Time
-}
-
 type FilterSet struct {
 	Operations []FilterOperation
 }
 
 func (fs *FilterSet) ParseFromQueries(values url.Values) error {
-	const layout = time.RFC3339
-
-	rangeMap := make(map[string]*RangeFilter)
-
 	for key, valArr := range values {
 		if !strings.HasPrefix(key, "filter[") || !strings.HasSuffix(key, "]") || len(valArr) == 0 {
 			continue
 		}
 
 		trimmed := strings.TrimSuffix(strings.TrimPrefix(key, "filter["), "]")
-		parts := strings.Split(trimmed, "][")
-
-		switch len(parts) {
-		case 2:
-			fs.Operations = append(fs.Operations, SimpleFilter{
-				FieldName: parts[0],
-				Op:        parts[1],
-				Value:     valArr[0],
-			})
-		case 3:
-			field := parts[0]
-			op := parts[1]
-			bound := parts[2]
-
-			if op != "range" {
-				continue
-			}
-
-			rf, exists := rangeMap[field]
-			if !exists {
-				rf = &RangeFilter{FieldName: field}
-				rangeMap[field] = rf
-			}
-
-			parsedTime, err := time.Parse(layout, valArr[0])
-			if err != nil {
-				return fmt.Errorf("invalid time format for field %s %s: %w", field, bound, err)
-			}
 
-			switch bound {
-			case "begin":
-				rf.Range.Begin = &parsedTime
-			case "end":
-				rf.Range.End = &parsedTime
-			}
+		parts := strings.Split(trimmed, "][")
+		if len(parts) != 2 {
+			continue
 		}
-	}
 
-	for _, rf := range rangeMap {
-		fs.Operations = append(fs.Operations, rf)
+		fs.Operations = append(fs.Operations, FilterOperation{
+			Field:     parts[0],
+			Operation: parts[1],
+			Value:     valArr[0],
+		})
 	}
-
 	return nil
 }
diff --git a/request/filter_test.go b/request/filter_test.go
index 91b971b..fca378f 100644
--- a/request/filter_test.go
+++ b/request/filter_test.go
@@ -3,65 +3,51 @@ package request
 import (
 	"net/url"
 	"testing"
-	"time"
 
 	"github.com/stretchr/testify/assert"
 )
 
 func TestFilterSet_ParseFromQueries(t *testing.T) {
 	values := url.Values{}
-	values.Set("filter[createdAt][gte]", "2024-01-01T00:00:00Z")
+	values.Set("filter[createdAt][gte]", "2024-01-01")
+	values.Set("filter[createdAt][lte]", "2024-02-01")
+	values.Set("filter[approve][eq]", "true")
 	values.Set("filter[amount][lte]", "1000")
 	values.Set("filter[status][eq]", "approved")
-	values.Set("filter[decisionEvaluatedAt][range][begin]", "2025-05-01T00:00:00Z")
-	values.Set("filter[decisionEvaluatedAt][range][end]", "2025-05-02T00:00:00Z")
 
 	var fs FilterSet
 	err := fs.ParseFromQueries(values)
 
 	assert.NoError(t, err)
-	assert.Len(t, fs.Operations, 4)
-
-	var foundCreatedAt, foundAmount, foundStatus, foundRange bool
-
-	for _, op := range fs.Operations {
-		switch f := op.(type) {
-		case SimpleFilter:
-			switch f.FieldName {
-			case "createdAt":
-				assert.Equal(t, "gte", f.Op)
-				assert.Equal(t, "2024-01-01T00:00:00Z", f.Value)
-				foundCreatedAt = true
-			case "amount":
-				assert.Equal(t, "lte", f.Op)
-				assert.Equal(t, "1000", f.Value)
-				foundAmount = true
-			case "status":
-				assert.Equal(t, "eq", f.Op)
-				assert.Equal(t, "approved", f.Value)
-				foundStatus = true
-			default:
-				t.Errorf("Unexpected SimpleFilter field: %s", f.FieldName)
-			}
-		case *RangeFilter:
-			assert.Equal(t, "decisionEvaluatedAt", f.FieldName)
-			assert.NotNil(t, f.Range.Begin)
-			assert.NotNil(t, f.Range.End)
-
-			beginExpected, _ := time.Parse(time.RFC3339, "2025-05-01T00:00:00Z")
-			endExpected, _ := time.Parse(time.RFC3339, "2025-05-02T00:00:00Z")
-
-			assert.True(t, f.Range.Begin.Equal(beginExpected))
-			assert.True(t, f.Range.End.Equal(endExpected))
-
-			foundRange = true
-		default:
-			t.Errorf("Unknown filter operation type %T", op)
-		}
-	}
-
-	assert.True(t, foundCreatedAt, "createdAt filter not found")
-	assert.True(t, foundAmount, "amount filter not found")
-	assert.True(t, foundStatus, "status filter not found")
-	assert.True(t, foundRange, "range filter not found")
+	assert.Len(t, fs.Operations, 5)
+
+	assert.Contains(t, fs.Operations, FilterOperation{
+		Field:     "createdAt",
+		Operation: "gte",
+		Value:     "2024-01-01",
+	})
+
+	assert.Contains(t, fs.Operations, FilterOperation{
+		Field:     "createdAt",
+		Operation: "lte",
+		Value:     "2024-02-01",
+	})
+
+	assert.Contains(t, fs.Operations, FilterOperation{
+		Field:     "approve",
+		Operation: "eq",
+		Value:     "1000",
+	})
+
+	assert.Contains(t, fs.Operations, FilterOperation{
+		Field:     "amount",
+		Operation: "lte",
+		Value:     "1000",
+	})
+
+	assert.Contains(t, fs.Operations, FilterOperation{
+		Field:     "status",
+		Operation: "eq",
+		Value:     "approved",
+	})
 }
-- 
GitLab


From af3a6384a84c8c2df2d97a9f30f95058fbfea5a6 Mon Sep 17 00:00:00 2001
From: "popov.dmitriy" <dp@sessia.dev>
Date: Thu, 15 May 2025 18:32:17 +0300
Subject: [PATCH 3/3] Refactoring

---
 request/filter_test.go          |  51 +++++++++++--
 request/group_test.go           |  19 ++++-
 request/pagination_test.go      | 131 +++++++++++++++-----------------
 response/{model.go => error.go} |  35 +--------
 response/pagination.go          |  32 ++++++++
 response/simple.go              |   9 +++
 6 files changed, 160 insertions(+), 117 deletions(-)
 rename response/{model.go => error.go} (64%)
 create mode 100644 response/pagination.go
 create mode 100644 response/simple.go

diff --git a/request/filter_test.go b/request/filter_test.go
index fca378f..ffd0fca 100644
--- a/request/filter_test.go
+++ b/request/filter_test.go
@@ -7,44 +7,79 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func TestFilterSet_ParseFromQueries(t *testing.T) {
+func TestFilterSet_ParseFromQueries_CreatedAtGte(t *testing.T) {
 	values := url.Values{}
 	values.Set("filter[createdAt][gte]", "2024-01-01")
-	values.Set("filter[createdAt][lte]", "2024-02-01")
-	values.Set("filter[approve][eq]", "true")
-	values.Set("filter[amount][lte]", "1000")
-	values.Set("filter[status][eq]", "approved")
 
 	var fs FilterSet
 	err := fs.ParseFromQueries(values)
 
 	assert.NoError(t, err)
-	assert.Len(t, fs.Operations, 5)
-
+	assert.Len(t, fs.Operations, 1)
 	assert.Contains(t, fs.Operations, FilterOperation{
 		Field:     "createdAt",
 		Operation: "gte",
 		Value:     "2024-01-01",
 	})
+}
 
+func TestFilterSet_ParseFromQueries_CreatedAtLte(t *testing.T) {
+	values := url.Values{}
+	values.Set("filter[createdAt][lte]", "2024-02-01")
+
+	var fs FilterSet
+	err := fs.ParseFromQueries(values)
+
+	assert.NoError(t, err)
+	assert.Len(t, fs.Operations, 1)
 	assert.Contains(t, fs.Operations, FilterOperation{
 		Field:     "createdAt",
 		Operation: "lte",
 		Value:     "2024-02-01",
 	})
+}
+
+func TestFilterSet_ParseFromQueries_ApproveEq(t *testing.T) {
+	values := url.Values{}
+	values.Set("filter[approve][eq]", "true")
+
+	var fs FilterSet
+	err := fs.ParseFromQueries(values)
 
+	assert.NoError(t, err)
+	assert.Len(t, fs.Operations, 1)
 	assert.Contains(t, fs.Operations, FilterOperation{
 		Field:     "approve",
 		Operation: "eq",
-		Value:     "1000",
+		Value:     "true",
 	})
+}
+
+func TestFilterSet_ParseFromQueries_AmountLte(t *testing.T) {
+	values := url.Values{}
+	values.Set("filter[amount][lte]", "1000")
+
+	var fs FilterSet
+	err := fs.ParseFromQueries(values)
 
+	assert.NoError(t, err)
+	assert.Len(t, fs.Operations, 1)
 	assert.Contains(t, fs.Operations, FilterOperation{
 		Field:     "amount",
 		Operation: "lte",
 		Value:     "1000",
 	})
+}
+
+func TestFilterSet_ParseFromQueries_StatusEq(t *testing.T) {
+	values := url.Values{}
+	values.Set("filter[status][eq]", "approved")
 
+	var fs FilterSet
+	err := fs.ParseFromQueries(values)
+
+	assert.NoError(t, err)
+	assert.Len(t, fs.Operations, 1)
 	assert.Contains(t, fs.Operations, FilterOperation{
 		Field:     "status",
 		Operation: "eq",
diff --git a/request/group_test.go b/request/group_test.go
index b748d36..705edb3 100644
--- a/request/group_test.go
+++ b/request/group_test.go
@@ -1,26 +1,37 @@
 package request
 
 import (
-	"github.com/stretchr/testify/assert"
 	"net/url"
 	"testing"
+
+	"github.com/stretchr/testify/assert"
 )
 
-func TestGroupingSet_ParseFromQueries(t *testing.T) {
+func TestGroupingSet_ParseFromQueries_CreatedAtMonth(t *testing.T) {
 	values := url.Values{}
 	values.Set("group[createdAt]", "month")
-	values.Set("group[status]", "raw")
 
 	var gs GroupingSet
 	err := gs.ParseFromQueries(values)
 
 	assert.NoError(t, err)
-	assert.Len(t, gs.Groups, 2)
+	assert.Len(t, gs.Groups, 1)
 
 	assert.Contains(t, gs.Groups, GroupingMethod{
 		Field:  "createdAt",
 		Method: "month",
 	})
+}
+
+func TestGroupingSet_ParseFromQueries_StatusRaw(t *testing.T) {
+	values := url.Values{}
+	values.Set("group[status]", "raw")
+
+	var gs GroupingSet
+	err := gs.ParseFromQueries(values)
+
+	assert.NoError(t, err)
+	assert.Len(t, gs.Groups, 1)
 
 	assert.Contains(t, gs.Groups, GroupingMethod{
 		Field:  "status",
diff --git a/request/pagination_test.go b/request/pagination_test.go
index 68e1fee..b6f4673 100644
--- a/request/pagination_test.go
+++ b/request/pagination_test.go
@@ -6,75 +6,64 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func TestPagination_Validate(t *testing.T) {
-	tests := []struct {
-		name          string
-		input         Pagination
-		expectedPage  int
-		expectedSize  int
-		expectError   bool
-		errorContains string
-	}{
-		{
-			name:         "default values set when zero",
-			input:        Pagination{Page: 0, PageSize: 0},
-			expectedPage: 1,
-			expectedSize: 10,
-			expectError:  false,
-		},
-		{
-			name:         "valid input",
-			input:        Pagination{Page: 2, PageSize: 50},
-			expectedPage: 2,
-			expectedSize: 50,
-			expectError:  false,
-		},
-		{
-			name:         "page less than minimum",
-			input:        Pagination{Page: 0, PageSize: 10},
-			expectedPage: 1, // default set
-			expectedSize: 10,
-			expectError:  false,
-		},
-		{
-			name:         "page size less than minimum",
-			input:        Pagination{Page: 1, PageSize: 0},
-			expectedPage: 1,
-			expectedSize: 10, // default set
-			expectError:  false,
-		},
-		{
-			name:          "page size too large",
-			input:         Pagination{Page: 1, PageSize: 101},
-			expectedPage:  1,
-			expectedSize:  101,
-			expectError:   true,
-			errorContains: "PageSize",
-		},
-		{
-			name:          "page less than 1 after default",
-			input:         Pagination{Page: -1, PageSize: 10},
-			expectedPage:  -1,
-			expectedSize:  10,
-			expectError:   true,
-			errorContains: "Page",
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			err := tt.input.Validate()
-			assert.Equal(t, tt.expectedPage, tt.input.Page)
-			assert.Equal(t, tt.expectedSize, tt.input.PageSize)
-
-			if tt.expectError {
-				assert.Error(t, err)
-				if err != nil {
-					assert.Contains(t, err.Error(), tt.errorContains)
-				}
-			} else {
-				assert.NoError(t, err)
-			}
-		})
-	}
+func TestPagination_Validate_DefaultValues(t *testing.T) {
+	p := Pagination{Page: 0, PageSize: 0}
+
+	err := p.Validate()
+
+	assert.NoError(t, err)
+	assert.Equal(t, 1, p.Page)
+	assert.Equal(t, 10, p.PageSize)
+}
+
+func TestPagination_Validate_ValidInput(t *testing.T) {
+	p := Pagination{Page: 2, PageSize: 50}
+
+	err := p.Validate()
+
+	assert.NoError(t, err)
+	assert.Equal(t, 2, p.Page)
+	assert.Equal(t, 50, p.PageSize)
+}
+
+func TestPagination_Validate_PageLessThanMinimum(t *testing.T) {
+	p := Pagination{Page: 0, PageSize: 10}
+
+	err := p.Validate()
+
+	assert.NoError(t, err)
+	assert.Equal(t, 1, p.Page)
+	assert.Equal(t, 10, p.PageSize)
+}
+
+func TestPagination_Validate_PageSizeLessThanMinimum(t *testing.T) {
+	p := Pagination{Page: 1, PageSize: 0}
+
+	err := p.Validate()
+
+	assert.NoError(t, err)
+	assert.Equal(t, 1, p.Page)
+	assert.Equal(t, 10, p.PageSize)
+}
+
+func TestPagination_Validate_PageSizeTooLarge(t *testing.T) {
+	p := Pagination{Page: 1, PageSize: 101}
+
+	err := p.Validate()
+
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "PageSize")
+	assert.Equal(t, 1, p.Page)
+	assert.Equal(t, 101, p.PageSize)
+}
+
+func TestPagination_Validate_PageNegative(t *testing.T) {
+	p := Pagination{Page: -1, PageSize: 10}
+
+	err := p.Validate()
+
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "Page")
+	assert.Equal(t, -1, p.Page)
+	assert.Equal(t, 10, p.PageSize)
 }
diff --git a/response/model.go b/response/error.go
similarity index 64%
rename from response/model.go
rename to response/error.go
index b0ff9af..5171d2b 100644
--- a/response/model.go
+++ b/response/error.go
@@ -1,39 +1,6 @@
 package response
 
-import (
-	"github.com/gofiber/fiber/v2"
-	"math"
-)
-
-func NewResponse(c *fiber.Ctx, data any) error {
-	return c.JSON(data)
-}
-
-type PaginationResponse struct {
-	Data       interface{} `json:"data"`
-	Pagination Pagination  `json:"pagination"`
-}
-
-type Pagination struct {
-	TotalPages  int   `json:"totalPages"`
-	CurrentPage int   `json:"currentPage"`
-	TotalItems  int64 `json:"totalItems"`
-	PageSize    int   `json:"pageSize"`
-}
-
-func NewPaginationResponse(c *fiber.Ctx, data any, page int, pageSize int, total int64) error {
-	totalPages := int(math.Ceil(float64(total) / float64(pageSize)))
-
-	return c.JSON(PaginationResponse{
-		Data: data,
-		Pagination: Pagination{
-			TotalPages:  totalPages,
-			CurrentPage: page,
-			TotalItems:  total,
-			PageSize:    pageSize,
-		},
-	})
-}
+import "github.com/gofiber/fiber/v2"
 
 type ErrorResponse struct {
 	Code    CodeErrorResponse `json:"code"`
diff --git a/response/pagination.go b/response/pagination.go
new file mode 100644
index 0000000..6534e93
--- /dev/null
+++ b/response/pagination.go
@@ -0,0 +1,32 @@
+package response
+
+import (
+	"github.com/gofiber/fiber/v2"
+	"math"
+)
+
+type PaginationResponse struct {
+	Data       interface{} `json:"data"`
+	Pagination Pagination  `json:"pagination"`
+}
+
+type Pagination struct {
+	TotalPages  int   `json:"totalPages"`
+	CurrentPage int   `json:"currentPage"`
+	TotalItems  int64 `json:"totalItems"`
+	PageSize    int   `json:"pageSize"`
+}
+
+func NewPaginationResponse(c *fiber.Ctx, data any, page int, pageSize int, total int64) error {
+	totalPages := int(math.Ceil(float64(total) / float64(pageSize)))
+
+	return c.JSON(PaginationResponse{
+		Data: data,
+		Pagination: Pagination{
+			TotalPages:  totalPages,
+			CurrentPage: page,
+			TotalItems:  total,
+			PageSize:    pageSize,
+		},
+	})
+}
diff --git a/response/simple.go b/response/simple.go
new file mode 100644
index 0000000..7085c12
--- /dev/null
+++ b/response/simple.go
@@ -0,0 +1,9 @@
+package response
+
+import (
+	"github.com/gofiber/fiber/v2"
+)
+
+func NewResponse(c *fiber.Ctx, data any) error {
+	return c.JSON(data)
+}
-- 
GitLab