Skip to content

Commit

Permalink
Merge pull request #8 from Trendyol/feature/swagger
Browse files Browse the repository at this point in the history
Feature/swagger
  • Loading branch information
ispiroglu authored Aug 13, 2024
2 parents 5b7f166 + 14f5744 commit b91618c
Show file tree
Hide file tree
Showing 33 changed files with 217,216 additions and 0 deletions.
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

0 comments on commit b91618c

Please sign in to comment.