Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/swagger #8

Merged
merged 3 commits into from
Aug 13, 2024
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 example/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@ package main
import (
"github.com/Trendyol/chaki"
"github.com/Trendyol/chaki/modules/server"
"github.com/Trendyol/chaki/modules/server/middlewares"
"github.com/Trendyol/chaki/modules/swagger"
)

func main() {
app := chaki.New()

app.Use(
server.Module(),
swagger.Module(),
)

app.Provide(
middlewares.ErrHandler,

// Controller
NewHelloController,
Expand Down
3 changes: 3 additions & 0 deletions modules/server/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ func Module() *module.Module {
// create registries
asController.Handler(parseControllers),

// swagger defs
getSwaggerDefs,

// middlewares - group
asMiddleware.Grouper(),

Expand Down
17 changes: 17 additions & 0 deletions modules/server/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package server
import (
"github.com/Trendyol/chaki/modules/server/controller"
"github.com/Trendyol/chaki/modules/server/route"
"github.com/Trendyol/chaki/modules/swagger"
"github.com/Trendyol/chaki/util/slc"
"github.com/gofiber/fiber/v2"
"net/url"
Expand Down Expand Up @@ -46,3 +47,19 @@ func (r *registry) toMeta(h route.Route) route.Meta {
m.Path = r.parsePath(m.Path)
return m
}

func (r *registry) SwaggerDefs() []swagger.EndpointDef {
metas := slc.Map(r.routes, r.toMeta)
return slc.Map(metas, r.toSwagDefinition)
}

func (r *registry) toSwagDefinition(m route.Meta) swagger.EndpointDef {
return swagger.EndpointDef{
RequestType: m.Req,
ResponseType: m.Res,
Group: r.name,
Name: m.Name,
Endpoint: m.Path,
Method: m.Method,
}
}
7 changes: 7 additions & 0 deletions modules/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/Trendyol/chaki/modules/server/controller"
"github.com/Trendyol/chaki/modules/server/middlewares"
"github.com/Trendyol/chaki/modules/server/route"
"github.com/Trendyol/chaki/modules/swagger"
"github.com/Trendyol/chaki/util/slc"
"github.com/gofiber/fiber/v2"
"net/http"
Expand Down Expand Up @@ -125,3 +126,9 @@ func setDefaultFiberConfigs(cfg *config.Config) {
serverCfg.SetDefault("readtimeout", "10s")
serverCfg.SetDefault("writetimeout", "10s")
}

func getSwaggerDefs(rs []*registry) []swagger.EndpointDef {
return slc.FlatMap(rs, func(r *registry) []swagger.EndpointDef {
return r.SwaggerDefs()
})
}
88 changes: 88 additions & 0 deletions modules/swagger/defs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package swagger

import (
"reflect"
"strings"

"github.com/Trendyol/chaki/internal/typlect"
"github.com/Trendyol/chaki/util/slc"
)

func buildDefinitions(eds []EndpointDef) m {
defs := make(m)
for _, ed := range eds {
buildModelDefinition(defs, ed.RequestType, true)
buildModelDefinition(defs, ed.ResponseType, false)
}

return defs

}

func buildModelDefinition(defs m, t reflect.Type, isReq bool) {
if t == typlect.TypeNoParam {
return
}

if t.Kind() == reflect.Slice {
t = t.Elem()
}

if t.Kind() == reflect.Pointer {
t = t.Elem()
}

if t.Kind() != reflect.Struct {
return
}

var smr []string
smp := m{}
for i := 0; i < t.NumField(); i++ {
var (
f = t.Field(i)
ft = f.Type
)

// build subtype definitions
if ft != typlect.TypeTime && ft.Kind() == reflect.Struct {
buildModelDefinition(defs, ft, isReq)
}

if ft.Kind() == reflect.Slice && ft.Elem().Kind() == reflect.Struct {
buildModelDefinition(defs, ft.Elem(), isReq)
}

if !isReq || f.Tag.Get("json") != "" {
smp[getFieldName(f)] = getPropertyField(f.Type)

if vts, ok := f.Tag.Lookup("validate"); isReq && ok {
if slc.Contains(strings.Split(vts, ","), "required") {
smr = append(smr, getFieldName(f))
}
}
}
}

if len(smp) > 0 {
mi := m{
"type": "object",
"properties": smp,
}

if len(smr) > 0 {
mi["required"] = smr
}

defs[getNameFromType(t)] = mi
}

}

func getFieldName(f reflect.StructField) string {
if tag := f.Tag.Get("json"); tag != "" {
return strings.Split(tag, ",")[0] // ignore ',omitempty'
}

return f.Name
}
167 changes: 167 additions & 0 deletions modules/swagger/defs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package swagger

import (
"reflect"
"testing"

"github.com/Trendyol/chaki/internal/typlect"
)

type testStruct struct {
Field1 string `json:"field1" validate:"required"`
Field2 int `json:"field2"`
Field3 anotherStruct `json:"field3"`
Field4 []anotherStruct `json:"field4"`
Field5 *string `json:"field5"`
Field6 map[string]int `json:"field6" validate:"required"`
}

type anotherStruct struct {
Field1 string `json:"field1"`
Field2 []int `json:"field2"`
}

func TestBuildDefinitions(t *testing.T) {
eds := []EndpointDef{
{
RequestType: reflect.TypeOf(testStruct{}),
ResponseType: reflect.TypeOf(anotherStruct{}),
},
{
RequestType: reflect.TypeOf(&testStruct{}),
ResponseType: reflect.TypeOf(&anotherStruct{}),
},
}

defs := buildDefinitions(eds)

if len(defs) != 2 {
t.Errorf("Expected 2 definitions, got %d", len(defs))
}

if _, ok := defs["testStruct"]; !ok {
t.Errorf("Expected testStruct in definitions, not found")
}

if _, ok := defs["anotherStruct"]; !ok {
t.Errorf("Expected anotherStruct in definitions, not found")
}
}

func TestBuildModelDefinition(t *testing.T) {
mockType := reflect.TypeOf(testStruct{})
defs := make(m)

buildModelDefinition(defs, mockType, true)

if len(defs) == 0 {
t.Errorf("Expected definitions to be populated, got empty map")
}

if _, ok := defs["testStruct"]; !ok {
t.Errorf("Expected testStruct in definitions, not found")
}

requiredFields := defs["testStruct"].(m)["required"].([]string)
if !reflect.DeepEqual(requiredFields, []string{"field1", "field6"}) {
t.Errorf("Expected required fields to be populated, got %+v", requiredFields)
}

if props, ok := defs["testStruct"].(m)["properties"]; ok {
if p, ok := props.(m)["field1"]; !ok {
t.Errorf("Expected field1 in properties, not found")
} else {
if fieldType, ok := p.(m)["type"]; !ok || fieldType != "string" {
t.Errorf("Expected field1 to be of type string")
}
}
if p, ok := props.(m)["field2"]; !ok {
t.Errorf("Expected field2 in properties, not found")
} else {
if fieldType, ok := p.(m)["type"]; !ok || fieldType != "integer" {
t.Errorf("Expected field2 to be of type integer")
}
}

if p, ok := props.(m)["field3"]; !ok {
t.Errorf("Expected Field3 in properties, not found")
} else {
if fieldType, ok := p.(m)["$ref"]; !ok || fieldType != "#/definitions/anotherStruct" {
t.Errorf("Expected field3 to be of type ref")
}
}

if p, ok := props.(m)["field4"]; !ok {
t.Errorf("Expected field4 in properties, not found")
} else {
if fieldType, ok := p.(m)["type"]; !ok || fieldType != "array" {
t.Errorf("Expected field4 to be of type array")
}
}

if p, ok := props.(m)["field5"]; !ok {
t.Errorf("Expected field5 in properties, not found")
} else {
if fieldType, ok := p.(m)["type"]; !ok || fieldType != "string" {
t.Errorf("Expected field5 to be of type string")
}
}

if p, ok := props.(m)["field6"]; !ok {
t.Errorf("Expected field6 in properties, not found")
} else {
if fieldType, ok := p.(m)["type"]; !ok || fieldType != "map" {
t.Errorf("Expected field6 to be of type map")
}
}
} else {
t.Errorf("Expected properties to be a map, got %T", defs["testStruct"].(m)["properties"])
}
}

func TestBuildModelDefinitions(t *testing.T) {

tests := []struct {
field reflect.Type
len int
name string
}{
{typlect.TypeNoParam, 0, "no param"},
{reflect.TypeOf([]string{}), 0, "slice"},

// As test struct contains one inner struct, there should be 2 different definitions
{reflect.TypeOf(&testStruct{}), 2, "pointer"},
{reflect.TypeOf(testStruct{}), 2, "struct"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

defs := make(m)
buildModelDefinition(defs, tt.field, true)

if len(defs) != tt.len {
t.Errorf("Expected %d definitions, got %d", tt.len, len(defs))
}
})
}
}

func TestGetFieldName(t *testing.T) {
tests := []struct {
field reflect.StructField
expected string
}{
{reflect.StructField{Name: "Field1", Tag: `json:"field1"`}, "field1"},
{reflect.StructField{Name: "Field2", Tag: `json:"field2,omitempty"`}, "field2"},
{reflect.StructField{Name: "Field3"}, "Field3"},
}

for _, tt := range tests {
t.Run(tt.expected, func(t *testing.T) {
if got := getFieldName(tt.field); got != tt.expected {
t.Errorf("getFieldName() = %v, want %v", got, tt.expected)
}
})
}
}
Binary file added modules/swagger/files/favicon-16x16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added modules/swagger/files/favicon-32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions modules/swagger/files/files.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package files

import "embed"

//go:embed *
var Files embed.FS
16 changes: 16 additions & 0 deletions modules/swagger/files/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}

*,
*:before,
*:after {
box-sizing: inherit;
}

body {
margin: 0;
background: #fafafa;
}
19 changes: 19 additions & 0 deletions modules/swagger/files/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
<link rel="stylesheet" type="text/css" href="./index.css" />
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
</head>

<body>
<div id="swagger-ui"></div>
<script src="./swagger-ui-bundle.js" charset="UTF-8"> </script>
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
<script src="./swagger-initializer.js" charset="UTF-8"> </script>
</body>
</html>
Loading