#2 - add swaggerset

This commit is contained in:
Gorbunov Kirill Andreevich
2024-11-29 19:40:12 +03:00
parent 82824cdfcc
commit 95acea00d2
12 changed files with 776 additions and 67 deletions

View File

@@ -1,14 +1,33 @@
package config
import (
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"github.com/google/uuid"
yamlcodec "go.unistack.org/micro-codec-yaml/v3"
mtime "go.unistack.org/micro/v3/util/time"
)
var Filesytem fs.FS
func init() {
dir, _ := os.Getwd()
Filesytem = os.DirFS(dir)
}
type Config struct {
App *AppConfig `json:"app,omitempty" yaml:"app,omitempty"`
Meter *MeterConfig `json:"meter,omitempty" yaml:"meter,omitempty"`
}
type AppConfig struct {
MultiUser bool `json:"multi_user,omitempty" yaml:"multi_user,omitempty"`
ChecksFiles []string `json:"checks_files,omitempty" yaml:"checks_files,omitempty"`
Checks []*CheckConfig `json:"checks,omitempty" yaml:"checks,omitempty"`
MultiUser bool `json:"multi_user,omitempty" yaml:"multi_user,omitempty"`
}
type MeterConfig struct {
@@ -16,19 +35,14 @@ type MeterConfig struct {
Path string `json:"path,omitempty" yaml:"path,omitempty"`
}
type Config struct {
App *AppConfig `json:"app,omitempty" yaml:"app,omitempty"`
Meter *MeterConfig `json:"meter,omitempty" yaml:"meter,omitempty"`
Checks []*CheckConfig `json:"checks,omitempty" yaml:"checks,omitempty"`
}
type CheckConfig struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Tasks []*TaskConfig `json:"tasks,omitempty" yaml:"tasks,omitempty"`
Timeout mtime.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"`
Interval mtime.Duration `json:"interval,omitempty" yaml:"interval,omitempty"`
Active bool `json:"active,omitempty" yaml:"active,omitempty"`
User string `json:"user,omitempty" yaml:"user,omitempty"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
User string `json:"user,omitempty" yaml:"user,omitempty"`
Tasks []*TaskConfig `json:"tasks,omitempty" yaml:"tasks,omitempty"`
TasksFiles []string `json:"tasks_files,omitempty" yaml:"tasks_files,omitempty"`
Timeout mtime.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"`
Interval mtime.Duration `json:"interval,omitempty" yaml:"interval,omitempty"`
Active bool `json:"active,omitempty" yaml:"active,omitempty"`
}
type HTTPConfig struct {
@@ -65,11 +79,84 @@ type TaskConfig struct {
Active bool `json:"active,omitempty" yaml:"active,omitempty"`
}
func (cfg *Config) Parse(r io.Reader) error {
buf, err := io.ReadAll(r)
func Load(fileSystem fs.FS, name string, cfg *Config) error {
if err := load(fileSystem, name, cfg); err != nil {
return err
}
for _, checksPatternFile := range cfg.App.ChecksFiles {
checkRoot := filepath.Dir(checksPatternFile)
checksFiles := fsWalkDir(fileSystem, checkRoot, checksPatternFile)
for _, checkFile := range checksFiles {
checks := []*CheckConfig{}
if err := load(fileSystem, checkFile, &checks); err != nil {
return err
}
for ckecksIdx := range checks {
for _, tasksPatternFile := range checks[ckecksIdx].TasksFiles {
taskRoot := filepath.Join(filepath.Dir(checksPatternFile), filepath.Dir(tasksPatternFile))
tasksFiles := fsWalkDir(fileSystem, taskRoot, filepath.Join(filepath.Dir(checksPatternFile), tasksPatternFile))
for tasksIdx := range tasksFiles {
tasks := []*TaskConfig{}
if err := load(fileSystem, tasksFiles[tasksIdx], &tasks); err != nil {
return err
}
checks[ckecksIdx].Tasks = append(checks[ckecksIdx].Tasks, tasks...)
}
}
cfg.App.Checks = append(cfg.App.Checks, checks[ckecksIdx])
}
}
}
if !cfg.App.MultiUser {
for _, check := range cfg.App.Checks {
check.User = uuid.Nil.String()
}
}
return nil
}
func fsWalkDir(fileSystem fs.FS, root string, pattern string) []string {
var files []string
fs.WalkDir(fileSystem, root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() || !d.Type().IsRegular() {
return nil
}
var ok bool
if ok, err = filepath.Match(pattern, path); err == nil && ok {
files = append(files, path)
} else {
return err
}
return nil
})
return files
}
func load(fileSystem fs.FS, name string, cfg interface{}) error {
f, err := fileSystem.Open(name)
if err != nil {
return err
}
return yamlcodec.NewCodec().Unmarshal(buf, cfg)
c := yamlcodec.NewCodec()
var buf []byte
if buf, err = io.ReadAll(f); err == nil {
if err = f.Close(); err == nil {
err = c.Unmarshal(buf, cfg)
}
}
if err != nil {
return fmt.Errorf("failed to load config %w", err)
}
return nil
}

View File

@@ -3,8 +3,8 @@
package grpcconn
import (
"github.com/emicklei/proto"
"github.com/jhump/protoreflect/desc"
"google.golang.org/protobuf/proto"
)
var protoSets = map[string]*desc.FileDescriptor

84
pkg/protoset/protoset.go Normal file
View File

@@ -0,0 +1,84 @@
package protoset
import (
"context"
"errors"
"fmt"
"sync"
protocodec "go.unistack.org/micro-codec-proto/v3"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/dynamicpb"
)
var errNotFound = errors.New("file descriptor not found")
type ProtoSet struct {
mu sync.Mutex
files map[string]*protoregistry.Files
}
func NewProtoSet() *ProtoSet {
return &ProtoSet{
mu: sync.Mutex{},
files: make(map[string]*protoregistry.Files, 0),
}
}
func (p *ProtoSet) GetMessage(addr, pkg, svc, mth string) (protoreflect.Message, protoreflect.Message, error) {
if addr == "" || svc == "" || mth == "" || pkg == "" {
return nil, nil, errors.New("addr or service name is empty")
}
p.mu.Lock()
pfile, ok := p.files[addr+"|"+svc]
p.mu.Unlock()
if !ok || pfile == nil {
return nil, nil, errNotFound
}
pdesc, err := pfile.FindDescriptorByName(protoreflect.FullName(pkg + "." + svc))
if err != nil {
return nil, nil, fmt.Errorf("failed to find service %s.%s, err: %w", pkg, svc, err)
}
sdesc, ok := pdesc.(protoreflect.ServiceDescriptor)
if !ok {
return nil, nil, fmt.Errorf("failed to find service " + pkg + "." + svc)
}
mdesc := sdesc.Methods().ByName(protoreflect.Name(mth))
if mdesc == nil {
return nil, nil, fmt.Errorf("unknown method " + mth)
}
req := dynamicpb.NewMessageType(mdesc.Input()).New()
rsp := dynamicpb.NewMessageType(mdesc.Output()).New()
return req, rsp, nil
}
func (p *ProtoSet) AddProtoset(addr, svc string, data []byte) error {
fdset := &descriptorpb.FileDescriptorSet{}
if err := protocodec.NewCodec().Unmarshal(data, fdset); err != nil {
return fmt.Errorf("failed to unmarshal protoset file: %w", err)
}
pfileoptions := protodesc.FileOptions{AllowUnresolvable: true}
pfiles, err := pfileoptions.NewFiles(fdset)
if err != nil {
return fmt.Errorf("failed to use protoset file, err: %w", err)
}
p.mu.Lock()
p.files[addr+"|"+svc] = pfiles
p.mu.Unlock()
return nil
}
func (p *ProtoSet) AddReflection(ctx context.Context, service string, addr string) error {
return nil
}

View File

@@ -0,0 +1,49 @@
package protoset
import (
"fmt"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestProtoSet_1(t *testing.T) {
p := NewProtoSet()
data, err := os.ReadFile("path to .protoset")
assert.Nil(t, err)
err = p.AddProtoset("localhost:9090", "CardService", data)
assert.Nil(t, err)
req, rsp, err := p.GetMessage("localhost:9090", "card_proto", "CardService", "GetCardList")
assert.Nil(t, err)
assert.NotNil(t, req)
assert.NotNil(t, rsp)
fmt.Printf("req: %v, rsp: %v \n", req, rsp)
}
func TestProtoSet_2_bad(t *testing.T) {
p := NewProtoSet()
data, err := os.ReadFile("path to .protoset")
assert.Nil(t, err)
err = p.AddProtoset("localhost:9090", "CardService", data)
assert.Nil(t, err)
req, rsp, err := p.GetMessage("localhost:9090", "card_proto", "Card", "GetCardList")
assert.Error(t, err)
assert.Nil(t, req)
assert.Nil(t, rsp)
fmt.Printf("req: %v, rsp: %v \n", req, rsp)
}
func TestProtoSet_3_not_found(t *testing.T) {
p := NewProtoSet()
req, rsp, err := p.GetMessage("localhost:9090", "card_proto", "CardService", "GetCardList")
assert.ErrorIs(t, err, errNotFound)
assert.Nil(t, req)
assert.Nil(t, rsp)
fmt.Printf("req: %v, rsp: %v \n", req, rsp)
}

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,79 @@
package swaggerset
import (
"context"
"errors"
"fmt"
"sync"
"github.com/getkin/kin-openapi/openapi3"
)
var errNotFound = errors.New("file descriptor not found")
type SwaggerSet struct {
mu sync.Mutex
files map[string]*openapi3.T
}
func NewSwaggerSet() *SwaggerSet {
return &SwaggerSet{
mu: sync.Mutex{},
files: make(map[string]*openapi3.T, 0),
}
}
func (p *SwaggerSet) GetMessage(addr, svc, mth, typereq string) (*messages, error) {
if svc == "" || mth == "" || addr == "" || typereq == "" {
return nil, errors.New("addr or service name is empty")
}
messages := newMessages()
p.mu.Lock()
doc := p.files[addr+"|"+svc]
p.mu.Unlock()
pathItem := doc.Paths.Value(mth)
if pathItem.Get != nil {
reqParam, reqBody, rsp := handleOperation("GET", pathItem.Get)
messages.Msgs = append(messages.Msgs, message{
Type: "GET",
RequestParam: reqParam,
RequestBody: reqBody,
Response: rsp,
})
}
if pathItem.Post != nil {
reqParam, reqBody, rsp := handleOperation("POST", pathItem.Post)
messages.Msgs = append(messages.Msgs, message{
Type: "POST",
RequestParam: reqParam,
RequestBody: reqBody,
Response: rsp,
})
}
return messages, nil
}
func (p *SwaggerSet) AddSwaggerset(addr, svc string, data []byte) error {
ctx := context.Background()
loader := openapi3.NewLoader()
loader.IsExternalRefsAllowed = true
doc, err := loader.LoadFromData(data)
if err != nil {
return fmt.Errorf("failed to load data from buf: %w", err)
}
if err = doc.Validate(ctx); err != nil {
return fmt.Errorf("failed to validate data from swagger: %w", err)
}
p.mu.Lock()
p.files[addr+"|"+svc] = doc
p.mu.Unlock()
return nil
}

View File

@@ -0,0 +1,40 @@
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)
msgs, err := s.GetMessage(
"localhost:8080",
"service",
"/domain-service/v1/push_mail/enabled",
"POST")
assert.Nil(t, err)
assert.NotNil(t, msgs.Msgs)
for _, msg := range msgs.Msgs {
reqParam, err := json.Marshal(msg.RequestParam)
assert.Nil(t, err)
reqBody, err := json.Marshal(msg.RequestBody)
assert.Nil(t, err)
rsp, err := json.Marshal(msg.Response)
assert.Nil(t, err)
fmt.Printf("type: %s, reqParam: %s, reqBody: %s, rsp: %s \n", msg.Type, reqParam, reqBody, rsp)
}
}
func TestSwaggerSet_2(t *testing.T) {
Run4()
}

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

@@ -0,0 +1,183 @@
package swaggerset
import (
"encoding/json"
"fmt"
"log"
"reflect"
"strings"
"github.com/getkin/kin-openapi/openapi3"
dynamicstruct "github.com/ompluscator/dynamic-struct"
)
type messages struct {
Msgs []message
}
func newMessages() *messages {
return &messages{
Msgs: make([]message, 0),
}
}
type message struct {
Type string
RequestParam interface{}
RequestBody interface{}
Response interface{}
}
func Run4() {
// Загружаем Swagger-документ
loader := openapi3.NewLoader()
doc, err := loader.LoadFromFile("swagger2.yaml")
if err != nil {
log.Fatalf("Ошибка загрузки Swagger-документа: %v", err)
}
// Пример обработки путей
for path, pathItem := range doc.Paths.Map() {
fmt.Printf("Path: %s\n", path)
// Пример обработки методов
if pathItem.Get != nil {
handleOperation("GET", pathItem.Get)
}
if pathItem.Post != nil {
handleOperation("POST", pathItem.Post)
}
}
}
// Обработка операции (GET или POST)
func handleOperation(method string, operation *openapi3.Operation) (reqParam, reqBody, rsp interface{}) {
fmt.Printf(" Method: %s\n", method)
// Обработка параметров (GET)
if len(operation.Parameters) > 0 {
paramsStruct := dynamicstruct.NewStruct()
for _, paramRef := range operation.Parameters {
param := paramRef.Value
fieldName := capitalize(param.Name)
goType := getGoType(param.Schema)
// В зависимости от того, где параметр находится (header, query, path, etc.), добавляем соответствующий тег
switch param.In {
case "query":
paramsStruct = paramsStruct.AddField(fieldName, goType, fmt.Sprintf(`json:"%s" query:"%s"`, param.Name, param.Name))
case "header":
paramsStruct = paramsStruct.AddField(fieldName, goType, fmt.Sprintf(`json:"%s" header:"%s"`, param.Name, param.Name))
default:
paramsStruct = paramsStruct.AddField(fieldName, goType, fmt.Sprintf(`json:"%s"`, param.Name))
}
}
requestStruct := paramsStruct.Build().New()
fmt.Printf(" Request (Parameters): %+v\n", requestStruct)
buf, _ := json.Marshal(requestStruct)
fmt.Printf(" Request (Parameters) JSON: %s\n", buf)
reqParam = requestStruct
}
// Обработка тела запроса (POST)
if operation.RequestBody != nil {
for _, content := range operation.RequestBody.Value.Content {
bodyFields := buildDynamicStruct(content.Schema)
bodyStruct := reflect.StructOf(bodyFields)
bodyInstance := reflect.New(bodyStruct).Interface()
fmt.Printf(" Request (Body): %+v\n", bodyInstance)
buf, _ := json.Marshal(bodyInstance)
fmt.Printf(" Request (Body) JSON: %s\n", buf)
reqBody = bodyInstance
}
}
// Обработка ответов
for status, response := range operation.Responses.Map() {
fmt.Printf(" Response Code: %s\n", status)
for _, content := range response.Value.Content {
responseFields := buildDynamicStruct(content.Schema)
responseStruct := reflect.StructOf(responseFields)
responseInstance := reflect.New(responseStruct).Interface()
fmt.Printf(" Response: %+v\n", responseInstance)
buf, _ := json.Marshal(responseInstance)
fmt.Printf(" Response JSON: %s\n", buf)
rsp = responseInstance
}
}
return
}
// Рекурсивное создание структуры из схемы с учетом $ref
func buildDynamicStruct(schema *openapi3.SchemaRef) []reflect.StructField {
var builder []reflect.StructField
fmt.Println("ref: ", schema.Ref)
if len(schema.Value.Properties) == 0 {
return builder
}
for name, prop := range schema.Value.Properties {
fieldName := capitalize(name)
if prop.Ref != "" || prop.Value.Type.Is("object") {
subBuilder := buildDynamicStruct(prop)
sfield := reflect.StructField{
Name: fieldName,
Type: reflect.StructOf(subBuilder),
Tag: reflect.StructTag(fmt.Sprintf(`json:"%s"`, name)),
}
builder = append(builder, sfield)
} else {
sfield := reflect.StructField{
Name: fieldName,
Type: reflect.TypeOf(getGoType(prop)),
Tag: reflect.StructTag(fmt.Sprintf(`json:"%s"`, name)),
}
builder = append(builder, sfield)
}
}
return builder
}
// Преобразование типа OpenAPI в тип Go
func getGoType(schema *openapi3.SchemaRef) interface{} {
switch {
case schema.Value.Type.Is("string"):
return ""
case schema.Value.Type.Is("integer"):
return 0
case schema.Value.Type.Is("boolean"):
return false
case schema.Value.Type.Is("array"):
return []interface{}{}
case schema.Value.Type.Is("object"):
return buildDynamicStruct(schema)
default:
return nil
}
}
func capitalize(fieldName string) string {
// Заменяем точки на подчеркивания для унификации
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])
}
// Собираем строку обратно, соединяя части без подчеркиваний
return strings.Join(parts, "")
}