diff --git a/cmd/servicechecker/main.go b/cmd/servicechecker/main.go index 6455cbd..1a30316 100644 --- a/cmd/servicechecker/main.go +++ b/cmd/servicechecker/main.go @@ -240,7 +240,7 @@ func newHTTPTask(ctx context.Context, l logger.Logger, m meter.Meter, check stri // client.WithContentType("application/json"), } - msg, err := swaggerSet.GetMessage(task.HTTP.Addr, svc, task.HTTP.Method, "POST") + msg, err := swaggerSet.GetMessage(task.HTTP.Addr, svc, task.HTTP.Method) if err != nil { l.Error(ctx, "failed to get message from swagger spec", err) return nil, nil, err diff --git a/go.mod b/go.mod index 6aee667..aee1f3d 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.23.3 require ( github.com/getkin/kin-openapi v0.128.0 github.com/go-co-op/gocron/v2 v2.12.3 - github.com/google/gnostic v0.7.0 github.com/google/uuid v1.6.0 github.com/ompluscator/dynamic-struct v1.4.0 github.com/stretchr/testify v1.9.0 @@ -17,6 +16,7 @@ require ( go.unistack.org/micro-codec-yaml/v3 v3.10.2 go.unistack.org/micro-meter-victoriametrics/v3 v3.8.9 go.unistack.org/micro-proto/v3 v3.4.1 + go.unistack.org/micro-proto/v4 v4.1.0 go.unistack.org/micro-server-http/v3 v3.11.34 go.unistack.org/micro/v3 v3.10.100 google.golang.org/protobuf v1.35.2 @@ -26,6 +26,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect + github.com/google/gnostic v0.7.0 // indirect github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect github.com/invopop/yaml v0.3.1 // indirect github.com/jonboulle/clockwork v0.4.0 // indirect diff --git a/go.sum b/go.sum index 9188338..f7f07a3 100644 --- a/go.sum +++ b/go.sum @@ -936,6 +936,8 @@ go.unistack.org/micro-meter-victoriametrics/v3 v3.8.9 h1:ZXCS0eFiSdvcFYxpxV2Q77g go.unistack.org/micro-meter-victoriametrics/v3 v3.8.9/go.mod h1:xODJQ0Nu/F8k34D/z2ITL91OskI/C674XCkugAxmc3Q= go.unistack.org/micro-proto/v3 v3.4.1 h1:UTjLSRz2YZuaHk9iSlVqqsA50JQNAEK2ZFboGqtEa9Q= go.unistack.org/micro-proto/v3 v3.4.1/go.mod h1:okx/cnOhzuCX0ggl/vToatbCupi0O44diiiLLsZ93Zo= +go.unistack.org/micro-proto/v4 v4.1.0 h1:qPwL2n/oqh9RE3RTTDgt28XK3QzV597VugQPaw9lKUk= +go.unistack.org/micro-proto/v4 v4.1.0/go.mod h1:ArmK7o+uFvxSY3dbJhKBBX4Pm1rhWdLEFf3LxBrMtec= go.unistack.org/micro-server-http/v3 v3.11.34 h1:Cu44/IWyTBa0I1LIt8TZu/z130uDs2t/OZJwV5H5NX0= go.unistack.org/micro-server-http/v3 v3.11.34/go.mod h1:3hlTQj3e6HQziJa2Coz/BwvVbt3oRejOevyAPUCuBEk= go.unistack.org/micro/v3 v3.10.94/go.mod h1:erMgt3Bl7vQQ0e9UpQyR5NlLiZ9pKeEJ9+1tfYFaqUg= diff --git a/pkg/swaggerset/swaggerset.go b/pkg/swaggerset/swaggerset.go index d3553ca..d3f9752 100644 --- a/pkg/swaggerset/swaggerset.go +++ b/pkg/swaggerset/swaggerset.go @@ -1,30 +1,29 @@ package swaggerset import ( - "context" "errors" - "fmt" + "net/http" "sync" - "github.com/getkin/kin-openapi/openapi3" + 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]*openapi3.T + files map[string]*openapi.Document } func NewSwaggerSet() *SwaggerSet { return &SwaggerSet{ mu: sync.Mutex{}, - files: make(map[string]*openapi3.T, 0), + files: make(map[string]*openapi.Document, 0), } } -func (p *SwaggerSet) GetMessage(addr, svc, mth string, typereq string) (*Message, error) { - if svc == "" || mth == "" || addr == "" || typereq == "" { +func (p *SwaggerSet) GetMessage(addr, svc, mth string) (*Message, error) { + if svc == "" || mth == "" || addr == "" { return nil, errors.New("addr or service name is empty") } @@ -32,10 +31,34 @@ func (p *SwaggerSet) GetMessage(addr, svc, mth string, typereq string) (*Message doc := p.files[addr+"|"+svc] p.mu.Unlock() - pathItem := doc.Paths.Value(mth) - reqParam, reqBody, rsp := handleOperation(typereq, pathItem.Get) + 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, + Type: typeReq, Request: httpRequest{ Header: reqParam, Body: reqBody, @@ -47,18 +70,9 @@ func (p *SwaggerSet) GetMessage(addr, svc, mth string, typereq string) (*Message } func (p *SwaggerSet) AddSwaggerset(addr, svc string, data []byte) error { - ctx := context.Background() - - loader := openapi3.NewLoader() - loader.IsExternalRefsAllowed = true - - doc, err := loader.LoadFromData(data) + doc, err := openapi.ParseDocument(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) + return err } p.mu.Lock() diff --git a/pkg/swaggerset/swaggerset_test.go b/pkg/swaggerset/swaggerset_test.go index cd97ced..513a39c 100644 --- a/pkg/swaggerset/swaggerset_test.go +++ b/pkg/swaggerset/swaggerset_test.go @@ -17,21 +17,17 @@ func TestSwaggerSet_1(t *testing.T) { err = s.AddSwaggerset("localhost:8080", "service", data) assert.Nil(t, err) - msgs, err := s.GetMessage( + msg, err := s.GetMessage( "localhost:8080", "service", "/domain-service/v1/push_mail/enabled", - []string{"POST", "GET"}) + "GET") 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("JSON: type: %s, reqParam: %s, reqBody: %s, rsp: %s \n", msg.Type, reqParam, reqBody, rsp) - fmt.Printf("Struct: type: %s, reqParam: %+v, reqBody: %+v, rsp: %+v \n", msg.Type, msg.RequestParam, msg.RequestBody, msg.Response) - } + 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) } diff --git a/pkg/swaggerset/util.go b/pkg/swaggerset/util.go index 2953f94..6291e66 100644 --- a/pkg/swaggerset/util.go +++ b/pkg/swaggerset/util.go @@ -5,8 +5,8 @@ import ( "reflect" "strings" - "github.com/getkin/kin-openapi/openapi3" dynamicstruct "github.com/ompluscator/dynamic-struct" + openapi "go.unistack.org/micro-proto/v4/openapiv3" ) type Message struct { @@ -20,17 +20,16 @@ type httpRequest struct { Body interface{} } -func handleOperation(method string, operation *openapi3.Operation) (reqParam, reqBody, rsp interface{}) { - fmt.Printf(" Method: %s\n", method) - +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 { - param := paramRef.Value + var param *openapi.Parameter + param = paramRef.GetParameter() fieldName := capitalize(param.Name) jsonName := strings.ToLower(param.Name[:1]) + param.Name[1:] - goType := getGoType(param.Schema) + goType := getGoType(doc, param.Schema) // В зависимости от того, где параметр находится (header, query, path, etc.), добавляем соответствующий тег switch param.In { @@ -47,9 +46,17 @@ func handleOperation(method string, operation *openapi3.Operation) (reqParam, re } // Обработка тела запроса (POST) - if operation.RequestBody != nil { - for _, content := range operation.RequestBody.Value.Content { - bodyFields := buildDynamicStruct(content.Schema) + 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() // Получили тело запроса @@ -58,10 +65,19 @@ func handleOperation(method string, operation *openapi3.Operation) (reqParam, re } // Обработка ответов - for status, response := range operation.Responses.Map() { - fmt.Printf(" Response Code: %s\n", status) - for _, content := range response.Value.Content { - responseFields := buildDynamicStruct(content.Schema) + 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() // Получили структуру ответа @@ -73,30 +89,42 @@ func handleOperation(method string, operation *openapi3.Operation) (reqParam, re } // Рекурсивное создание структуры из схемы с учетом $ref -func buildDynamicStruct(schema *openapi3.SchemaRef) []reflect.StructField { +func buildDynamicStruct(doc *openapi.Document, schemaOrRef *openapi.SchemaOrReference) []reflect.StructField { var sfields []reflect.StructField - - if len(schema.Value.Properties) == 0 { - return sfields + 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 name, prop := range schema.Value.Properties { - fieldName := capitalize(name) - if prop.Ref != "" || prop.Value.Type.Is("object") { - subBuilder := buildDynamicStruct(prop) + 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"`, name)), + Tag: reflect.StructTag(fmt.Sprintf(`json:"%s"`, prop.GetName())), } sfields = append(sfields, sfield) - } else { + } + if prop.GetValue().GetSchema() != nil { sfield := reflect.StructField{ Name: fieldName, - Type: reflect.TypeOf(getGoType(prop)), - Tag: reflect.StructTag(fmt.Sprintf(`json:"%s"`, name)), + Type: reflect.TypeOf(getGoType(doc, prop.GetValue())), + Tag: reflect.StructTag(fmt.Sprintf(`json:"%s"`, prop.GetName())), } sfields = append(sfields, sfield) @@ -106,25 +134,41 @@ func buildDynamicStruct(schema *openapi3.SchemaRef) []reflect.StructField { 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(schema *openapi3.SchemaRef) interface{} { - switch { - case schema.Value.Type.Is("string"): +func getGoType(doc *openapi.Document, schema *openapi.SchemaOrReference) interface{} { + switch schema.GetSchema().Type { + case "string": return "" - case schema.Value.Type.Is("integer"): + case "integer": return 0 - case schema.Value.Type.Is("boolean"): + case "boolean": return false - case schema.Value.Type.Is("array"): + case "array": return []interface{}{} - case schema.Value.Type.Is("object"): - return buildDynamicStruct(schema) + case "object": + return buildDynamicStruct(doc, schema) default: return nil } } func capitalize(fieldName string) string { + if fieldName == "" { + return fieldName + } // Заменяем точки на подчеркивания для унификации fieldName = strings.ReplaceAll(fieldName, ".", "_")