#2 - add swaggerset (#8)

Co-authored-by: Gorbunov Kirill Andreevich <kgorbunov@mtsbank.ru>
Reviewed-on: #8
Reviewed-by: Василий Толстов <v.tolstov@unistack.org>
Co-authored-by: Кирилл Горбунов <kirya_gorbunov_2015@mail.ru>
Co-committed-by: Кирилл Горбунов <kirya_gorbunov_2015@mail.ru>
This commit is contained in:
Кирилл Горбунов
2024-12-06 19:01:07 +03:00
parent ce57938ec2
commit 7a302ce899
7 changed files with 439 additions and 8 deletions

View File

@@ -0,0 +1,81 @@
openapi: 3.0.3
info:
title: platform/services/domain/service-proto
description: Domain Service
version: 3.6.0
paths:
/domain-service/v1/push_mail/enabled:
get:
tags:
- DomainService
description: Получение статуса подключения PUSH (глобального)
operationId: IsPushTokenEnabled
parameters:
- name: Phone
in: header
schema:
type: string
- name: app_name
in: query
schema:
type: string
- name: device_id.value
in: query
description: The string value.
schema:
type: string
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/IsPushTokenEnabledRsp'
post:
tags:
- DomainService
description: Сохранение статуса подключения PUSH (глобального)
operationId: SetPushTokenEnabled
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/SetPushTokenEnabledReq'
required: true
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/SetPushTokenEnabledRsp'
components:
schemas:
IsPushTokenEnabledRsp:
type: object
properties:
result:
type: boolean
setPushTokenEnabledRsp:
$ref: '#/components/schemas/SetPushTokenEnabledRsp'
SetPushTokenEnabledReq:
type: object
properties:
app_name:
type: string
enabled:
type: boolean
device_id:
$ref: '#/components/schemas/StringValue'
StringValue:
type: object
properties:
value:
type: string
description: The string value.
description: Wrapper message for `string`. The JSON representation for `StringValue` is JSON string.
SetPushTokenEnabledRsp:
type: object
properties:
result:
type: boolean

View File

@@ -0,0 +1,82 @@
package swaggerset
import (
"errors"
"net/http"
"sync"
openapi "go.unistack.org/micro-proto/v4/openapiv3"
)
var errNotFound = errors.New("file descriptor not found")
type SwaggerSet struct {
mu sync.Mutex
files map[string]*openapi.Document
}
func NewSwaggerSet() *SwaggerSet {
return &SwaggerSet{
mu: sync.Mutex{},
files: make(map[string]*openapi.Document, 0),
}
}
func (p *SwaggerSet) GetMessage(addr, svc, mth string) (*Message, error) {
if svc == "" || mth == "" || addr == "" {
return nil, errors.New("addr or service name is empty")
}
p.mu.Lock()
doc := p.files[addr+"|"+svc]
p.mu.Unlock()
var reqParam, reqBody, rsp interface{}
var typeReq string
for _, path := range doc.Paths.GetPath() {
if path.GetName() == mth {
if path.GetValue().GetGet() != nil {
typeReq = http.MethodGet
reqParam, reqBody, rsp = handleOperation(path.GetValue().GetGet(), doc)
}
if path.GetValue().GetPost() != nil {
typeReq = http.MethodPost
reqParam, reqBody, rsp = handleOperation(path.GetValue().GetPost(), doc)
}
if path.GetValue().GetDelete() != nil {
typeReq = http.MethodDelete
reqParam, reqBody, rsp = handleOperation(path.GetValue().GetDelete(), doc)
}
if path.GetValue().GetPatch() != nil {
typeReq = http.MethodPatch
reqParam, reqBody, rsp = handleOperation(path.GetValue().GetPatch(), doc)
}
if path.GetValue().GetPut() != nil {
typeReq = http.MethodPut
reqParam, reqBody, rsp = handleOperation(path.GetValue().GetPut(), doc)
}
}
}
msg := &Message{
Type: typeReq,
Request: httpRequest{
Header: reqParam,
Body: reqBody,
},
Response: rsp,
}
return msg, nil
}
func (p *SwaggerSet) AddSwaggerset(addr, svc string, data []byte) error {
doc, err := openapi.ParseDocument(data)
if err != nil {
return err
}
p.mu.Lock()
p.files[addr+"|"+svc] = doc
p.mu.Unlock()
return nil
}

View File

@@ -0,0 +1,33 @@
package swaggerset
import (
"encoding/json"
"fmt"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSwaggerSet_1(t *testing.T) {
s := NewSwaggerSet()
data, err := os.ReadFile("swagger.yaml")
assert.Nil(t, err)
err = s.AddSwaggerset("localhost:8080", "service", data)
assert.Nil(t, err)
msg, err := s.GetMessage(
"localhost:8080",
"service",
"/domain-service/v1/push_mail/enabled",
"GET")
assert.Nil(t, err)
assert.NotNil(t, msg)
req, err := json.Marshal(msg.Request)
assert.Nil(t, err)
rsp, err := json.Marshal(msg.Response)
assert.Nil(t, err)
fmt.Printf("JSON: type: %s, req: %s, rsp: %s \n", msg.Type, req, rsp)
fmt.Printf("Struct: type: %s, req: %+v, rsp: %+v \n", msg.Type, msg.Request, msg.Response)
}

189
pkg/swaggerset/util.go Normal file
View File

@@ -0,0 +1,189 @@
package swaggerset
import (
"fmt"
"reflect"
"strings"
dynamicstruct "github.com/ompluscator/dynamic-struct"
openapi "go.unistack.org/micro-proto/v4/openapiv3"
)
type Message struct {
Type string
Request httpRequest
Response interface{}
}
type httpRequest struct {
Header interface{}
Body interface{}
}
func handleOperation(operation *openapi.Operation, doc *openapi.Document) (reqParam, reqBody, rsp interface{}) {
// Обработка параметров (GET)
if len(operation.Parameters) > 0 {
paramsStruct := dynamicstruct.NewStruct()
for _, paramRef := range operation.Parameters {
var param *openapi.Parameter
param = paramRef.GetParameter()
fieldName := capitalize(param.Name)
jsonName := strings.ToLower(param.Name[:1]) + param.Name[1:]
goType := getGoType(doc, param.Schema)
// В зависимости от того, где параметр находится (header, query, path, etc.), добавляем соответствующий тег
switch param.In {
case "query":
paramsStruct = paramsStruct.AddField(fieldName, goType, fmt.Sprintf(`json:"%s" query:"%s"`, jsonName, jsonName))
case "header":
paramsStruct = paramsStruct.AddField(fieldName, goType, fmt.Sprintf(`json:"%s" header:"%s"`, jsonName, jsonName))
default:
paramsStruct = paramsStruct.AddField(fieldName, goType, fmt.Sprintf(`json:"%s"`, jsonName))
}
}
// Получили структуру запроса для методов, где есть параметры в header, query, path, etc., добавили теги
reqParam = paramsStruct.Build().New()
}
// Обработка тела запроса (POST)
if operation.GetRequestBody() != nil {
if operation.GetRequestBody().GetRequestBody() != nil {
bodyFields := buildDynamicStruct(doc, operation.GetRequestBody().GetRequestBody().GetContent().GetAdditionalProperties()[0].GetValue().GetSchema())
bodyStruct := reflect.StructOf(bodyFields)
bodyInstance := reflect.New(bodyStruct).Interface()
// Получили тело запроса
reqBody = bodyInstance
}
if operation.GetRequestBody().GetReference() != nil {
schemOrRef := findReference(doc, operation.GetRequestBody().GetReference().GetXRef())
bodyFields := buildDynamicStruct(doc, schemOrRef)
bodyStruct := reflect.StructOf(bodyFields)
bodyInstance := reflect.New(bodyStruct).Interface()
// Получили тело запроса
reqBody = bodyInstance
}
}
// Обработка ответов
for _, rspOrRef := range operation.Responses.ResponseOrReference {
if rspOrRef.GetValue().GetResponse() != nil {
for _, prop := range rspOrRef.Value.GetResponse().GetContent().GetAdditionalProperties() {
responseFields := buildDynamicStruct(doc, prop.GetValue().GetSchema())
responseStruct := reflect.StructOf(responseFields)
responseInstance := reflect.New(responseStruct).Interface()
// Получили структуру ответа
rsp = responseInstance
}
}
if rspOrRef.GetValue().GetReference() != nil {
schemaOrRef := findReference(doc, rspOrRef.GetValue().GetReference().GetXRef())
responseFields := buildDynamicStruct(doc, schemaOrRef)
responseStruct := reflect.StructOf(responseFields)
responseInstance := reflect.New(responseStruct).Interface()
// Получили структуру ответа
rsp = responseInstance
}
}
return
}
// Рекурсивное создание структуры из схемы с учетом $ref
func buildDynamicStruct(doc *openapi.Document, schemaOrRef *openapi.SchemaOrReference) []reflect.StructField {
var sfields []reflect.StructField
var schema *openapi.Schema
if schemaOrRef.GetSchema() != nil {
schema = schemaOrRef.GetSchema()
}
if schemaOrRef.GetReference() != nil {
name := strings.Split(schemaOrRef.GetReference().GetXRef(), "#/components/schemas/")[1]
fieldName := capitalize(name)
subBuilder := buildDynamicStruct(doc, findReference(doc, schemaOrRef.GetReference().GetXRef()))
sfield := reflect.StructField{
Name: fieldName,
Type: reflect.StructOf(subBuilder),
Tag: reflect.StructTag(fmt.Sprintf(`json:"%s"`, name)),
}
sfields = append(sfields, sfield)
}
for _, prop := range schema.GetProperties().GetAdditionalProperties() {
fieldName := capitalize(prop.GetName())
if prop.GetValue().GetReference() != nil {
subBuilder := buildDynamicStruct(doc, prop.GetValue())
sfield := reflect.StructField{
Name: fieldName,
Type: reflect.StructOf(subBuilder),
Tag: reflect.StructTag(fmt.Sprintf(`json:"%s"`, prop.GetName())),
}
sfields = append(sfields, sfield)
}
if prop.GetValue().GetSchema() != nil {
sfield := reflect.StructField{
Name: fieldName,
Type: reflect.TypeOf(getGoType(doc, prop.GetValue())),
Tag: reflect.StructTag(fmt.Sprintf(`json:"%s"`, prop.GetName())),
}
sfields = append(sfields, sfield)
}
}
return sfields
}
func findReference(doc *openapi.Document, ref string) *openapi.SchemaOrReference {
var result *openapi.SchemaOrReference
ref = strings.Split(ref, "#/components/schemas/")[1]
for _, prop := range doc.Components.Schemas.GetAdditionalProperties() {
if prop.Name == ref {
result = prop.Value
}
}
return result
}
// Преобразование типа OpenAPI в тип Go
func getGoType(doc *openapi.Document, schema *openapi.SchemaOrReference) interface{} {
switch schema.GetSchema().Type {
case "string":
return ""
case "integer":
return 0
case "boolean":
return false
case "array":
return []interface{}{}
case "object":
return buildDynamicStruct(doc, schema)
default:
return nil
}
}
func capitalize(fieldName string) string {
if fieldName == "" {
return fieldName
}
// Заменяем точки на подчеркивания для унификации
fieldName = strings.ReplaceAll(fieldName, ".", "_")
// Разделяем строку по подчеркиваниям
parts := strings.Split(fieldName, "_")
if len(parts) == 1 {
return strings.ToUpper(fieldName[:1]) + fieldName[1:]
}
// Обрабатываем каждый фрагмент
for i := 0; i < len(parts); i++ {
// Капитализируем первые буквы всех частей, кроме первой
parts[i] = strings.Title(parts[i]) //cases.Title(language.English).String(parts[i])
}
// Собираем строку обратно, соединяя части без подчеркиваний
return strings.Join(parts, "")
}