diff --git a/Dockerfile b/Dockerfile index 4bd0df8f..b734a57e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,9 @@ RUN go mod download COPY internal internal COPY cmd cmd COPY pkg pkg +RUN mkdir -p /tmp/openapi +COPY api/openapi.json /tmp/openapi/openapi.json +COPY api/openapi.yml /tmp/openapi/openapi.yml RUN apt-get update && \ apt-get install -y libsecp256k1-0 libsodium23 @@ -20,4 +23,6 @@ RUN mkdir -p /app/lib RUN wget -O /app/lib/libemulator.so https://github.com/ton-blockchain/ton/releases/download/v2024.08/libemulator-linux-x86_64.so ENV LD_LIBRARY_PATH=/app/lib/ COPY --from=gobuild /tmp/opentonapi /usr/bin/ +COPY --from=gobuild /tmp/openapi /app/openapi +WORKDIR /app CMD ["/usr/bin/opentonapi"] diff --git a/api/openapi.json b/api/openapi.json index 3056bd32..e0c1bad4 100644 --- a/api/openapi.json +++ b/api/openapi.json @@ -9829,6 +9829,51 @@ ] } }, + "/v2/openapi.json": { + "get": { + "description": "Get the openapi.json file", + "operationId": "getOpenapiJson", + "responses": { + "200": { + "content": { + "application/json": { + "schema": {} + } + } + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "tags": [ + "Openapi" + ] + } + }, + "/v2/openapi.yml": { + "get": { + "description": "Get the openapi.yml file", + "operationId": "getOpenapiYml", + "responses": { + "200": { + "content": { + "application/x-yaml": { + "schema": { + "format": "binary", + "type": "string" + } + } + } + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "tags": [ + "Openapi" + ] + } + }, "/v2/pubkeys/{public_key}/wallets": { "get": { "description": "Get wallets by public key", diff --git a/api/openapi.yml b/api/openapi.yml index f7f1be40..587399f5 100644 --- a/api/openapi.yml +++ b/api/openapi.yml @@ -86,6 +86,34 @@ tags: url: https://docs.tonconsole.com/tonapi/rest-api/utilities paths: + /v2/openapi.json: + get: + description: Get the openapi.json file + operationId: getOpenapiJson + tags: + - Openapi + responses: + '200': + content: + application/json: + schema: { } # Free-form JSON value + 'default': + $ref: '#/components/responses/Error' + /v2/openapi.yml: + get: + description: Get the openapi.yml file + operationId: getOpenapiYml + tags: + - Openapi + responses: + '200': + content: + application/x-yaml: + schema: + type: string + format: binary + 'default': + $ref: '#/components/responses/Error' /v2/status: get: description: Status diff --git a/pkg/api/openapi.go b/pkg/api/openapi.go new file mode 100644 index 00000000..175e64b7 --- /dev/null +++ b/pkg/api/openapi.go @@ -0,0 +1,33 @@ +package api + +import ( + "bytes" + "context" + "github.com/go-faster/jx" + "github.com/tonkeeper/opentonapi/pkg/oas" + "net/http" + "os" +) + +func (h *Handler) GetOpenapiJson(ctx context.Context) (jx.Raw, error) { + file, err := os.ReadFile("openapi/openapi.json") + if err != nil { + return jx.Raw{}, toError(http.StatusInternalServerError, err) + } + d := jx.DecodeBytes(file) + result, err := d.Raw() + if err != nil { + return jx.Raw{}, toError(http.StatusInternalServerError, err) + } + return result, nil +} + +func (h *Handler) GetOpenapiYml(ctx context.Context) (oas.GetOpenapiYmlOK, error) { + file, err := os.ReadFile("openapi/openapi.yml") + if err != nil { + return oas.GetOpenapiYmlOK{}, toError(http.StatusInternalServerError, err) + } + return oas.GetOpenapiYmlOK{ + Data: bytes.NewReader(file), + }, nil +} diff --git a/pkg/oas/oas_handlers_gen.go b/pkg/oas/oas_handlers_gen.go index aa846c9b..7c360a7d 100644 --- a/pkg/oas/oas_handlers_gen.go +++ b/pkg/oas/oas_handlers_gen.go @@ -8,6 +8,7 @@ import ( "time" "github.com/go-faster/errors" + "github.com/go-faster/jx" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/metric" @@ -8549,6 +8550,202 @@ func (s *Server) handleGetNftItemsByAddressesRequest(args [0]string, argsEscaped } } +// handleGetOpenapiJsonRequest handles getOpenapiJson operation. +// +// Get the openapi.json file. +// +// GET /v2/openapi.json +func (s *Server) handleGetOpenapiJsonRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("getOpenapiJson"), + semconv.HTTPMethodKey.String("GET"), + semconv.HTTPRouteKey.String("/v2/openapi.json"), + } + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), "GetOpenapiJson", + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + // Use floating point division here for higher precision (instead of Millisecond method). + s.duration.Record(ctx, float64(float64(elapsedDuration)/float64(time.Millisecond)), metric.WithAttributes(otelAttrs...)) + }() + + // Increment request counter. + s.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + s.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + } + err error + ) + + var response jx.Raw + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: "GetOpenapiJson", + OperationSummary: "", + OperationID: "getOpenapiJson", + Body: nil, + Params: middleware.Parameters{}, + Raw: r, + } + + type ( + Request = struct{} + Params = struct{} + Response = jx.Raw + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + nil, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.GetOpenapiJson(ctx) + return response, err + }, + ) + } else { + response, err = s.h.GetOpenapiJson(ctx) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w, span); err != nil { + recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil { + recordError("Internal", err) + } + return + } + + if err := encodeGetOpenapiJsonResponse(response, w, span); err != nil { + recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleGetOpenapiYmlRequest handles getOpenapiYml operation. +// +// Get the openapi.yml file. +// +// GET /v2/openapi.yml +func (s *Server) handleGetOpenapiYmlRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("getOpenapiYml"), + semconv.HTTPMethodKey.String("GET"), + semconv.HTTPRouteKey.String("/v2/openapi.yml"), + } + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), "GetOpenapiYml", + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + // Use floating point division here for higher precision (instead of Millisecond method). + s.duration.Record(ctx, float64(float64(elapsedDuration)/float64(time.Millisecond)), metric.WithAttributes(otelAttrs...)) + }() + + // Increment request counter. + s.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + s.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + } + err error + ) + + var response GetOpenapiYmlOK + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: "GetOpenapiYml", + OperationSummary: "", + OperationID: "getOpenapiYml", + Body: nil, + Params: middleware.Parameters{}, + Raw: r, + } + + type ( + Request = struct{} + Params = struct{} + Response = GetOpenapiYmlOK + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + nil, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.GetOpenapiYml(ctx) + return response, err + }, + ) + } else { + response, err = s.h.GetOpenapiYml(ctx) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w, span); err != nil { + recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil { + recordError("Internal", err) + } + return + } + + if err := encodeGetOpenapiYmlResponse(response, w, span); err != nil { + recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + // handleGetOutMsgQueueSizesRequest handles getOutMsgQueueSizes operation. // // Get out msg queue sizes. diff --git a/pkg/oas/oas_response_encoders_gen.go b/pkg/oas/oas_response_encoders_gen.go index 963c42bd..faa1e2ab 100644 --- a/pkg/oas/oas_response_encoders_gen.go +++ b/pkg/oas/oas_response_encoders_gen.go @@ -3,6 +3,7 @@ package oas import ( + "io" "net/http" "github.com/go-faster/errors" @@ -986,6 +987,35 @@ func encodeGetNftItemsByAddressesResponse(response *NftItems, w http.ResponseWri return nil } +func encodeGetOpenapiJsonResponse(response jx.Raw, w http.ResponseWriter, span trace.Span) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(200) + span.SetStatus(codes.Ok, http.StatusText(200)) + + e := new(jx.Encoder) + if len(response) != 0 { + e.Raw(response) + } + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil +} + +func encodeGetOpenapiYmlResponse(response GetOpenapiYmlOK, w http.ResponseWriter, span trace.Span) error { + w.Header().Set("Content-Type", "application/x-yaml") + w.WriteHeader(200) + span.SetStatus(codes.Ok, http.StatusText(200)) + + writer := w + if _, err := io.Copy(writer, response); err != nil { + return errors.Wrap(err, "write") + } + + return nil +} + func encodeGetOutMsgQueueSizesResponse(response *GetOutMsgQueueSizesOK, w http.ResponseWriter, span trace.Span) error { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(200) diff --git a/pkg/oas/oas_router_gen.go b/pkg/oas/oas_router_gen.go index 9cb50cf2..9770d744 100644 --- a/pkg/oas/oas_router_gen.go +++ b/pkg/oas/oas_router_gen.go @@ -2657,6 +2657,63 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { elem = origElem } + elem = origElem + case 'o': // Prefix: "openapi." + origElem := elem + if l := len("openapi."); len(elem) >= l && elem[0:l] == "openapi." { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + break + } + switch elem[0] { + case 'j': // Prefix: "json" + origElem := elem + if l := len("json"); len(elem) >= l && elem[0:l] == "json" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "GET": + s.handleGetOpenapiJsonRequest([0]string{}, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "GET") + } + + return + } + + elem = origElem + case 'y': // Prefix: "yml" + origElem := elem + if l := len("yml"); len(elem) >= l && elem[0:l] == "yml" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "GET": + s.handleGetOpenapiYmlRequest([0]string{}, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "GET") + } + + return + } + + elem = origElem + } + elem = origElem case 'p': // Prefix: "pubkeys/" origElem := elem @@ -6133,6 +6190,71 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { elem = origElem } + elem = origElem + case 'o': // Prefix: "openapi." + origElem := elem + if l := len("openapi."); len(elem) >= l && elem[0:l] == "openapi." { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + break + } + switch elem[0] { + case 'j': // Prefix: "json" + origElem := elem + if l := len("json"); len(elem) >= l && elem[0:l] == "json" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + switch method { + case "GET": + // Leaf: GetOpenapiJson + r.name = "GetOpenapiJson" + r.summary = "" + r.operationID = "getOpenapiJson" + r.pathPattern = "/v2/openapi.json" + r.args = args + r.count = 0 + return r, true + default: + return + } + } + + elem = origElem + case 'y': // Prefix: "yml" + origElem := elem + if l := len("yml"); len(elem) >= l && elem[0:l] == "yml" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + switch method { + case "GET": + // Leaf: GetOpenapiYml + r.name = "GetOpenapiYml" + r.summary = "" + r.operationID = "getOpenapiYml" + r.pathPattern = "/v2/openapi.yml" + r.args = args + r.count = 0 + return r, true + default: + return + } + } + + elem = origElem + } + elem = origElem case 'p': // Prefix: "pubkeys/" origElem := elem diff --git a/pkg/oas/oas_schemas_gen.go b/pkg/oas/oas_schemas_gen.go index 24656402..8e93e24c 100644 --- a/pkg/oas/oas_schemas_gen.go +++ b/pkg/oas/oas_schemas_gen.go @@ -4,6 +4,7 @@ package oas import ( "fmt" + "io" "github.com/go-faster/errors" "github.com/go-faster/jx" @@ -5977,6 +5978,20 @@ func (s *GetNftItemsByAddressesReq) SetAccountIds(val []string) { s.AccountIds = val } +type GetOpenapiYmlOK struct { + Data io.Reader +} + +// Read reads data from the Data reader. +// +// Kept to satisfy the io.Reader interface. +func (s GetOpenapiYmlOK) Read(p []byte) (n int, err error) { + if s.Data == nil { + return 0, io.EOF + } + return s.Data.Read(p) +} + type GetOutMsgQueueSizesOK struct { ExtMsgQueueSizeLimit uint32 `json:"ext_msg_queue_size_limit"` Shards []GetOutMsgQueueSizesOKShardsItem `json:"shards"` diff --git a/pkg/oas/oas_server_gen.go b/pkg/oas/oas_server_gen.go index 1fcc5647..c06b8774 100644 --- a/pkg/oas/oas_server_gen.go +++ b/pkg/oas/oas_server_gen.go @@ -4,6 +4,8 @@ package oas import ( "context" + + "github.com/go-faster/jx" ) // Handler handles operations described by OpenAPI v3 specification. @@ -444,6 +446,18 @@ type Handler interface { // // POST /v2/nfts/_bulk GetNftItemsByAddresses(ctx context.Context, req OptGetNftItemsByAddressesReq) (*NftItems, error) + // GetOpenapiJson implements getOpenapiJson operation. + // + // Get the openapi.json file. + // + // GET /v2/openapi.json + GetOpenapiJson(ctx context.Context) (jx.Raw, error) + // GetOpenapiYml implements getOpenapiYml operation. + // + // Get the openapi.yml file. + // + // GET /v2/openapi.yml + GetOpenapiYml(ctx context.Context) (GetOpenapiYmlOK, error) // GetOutMsgQueueSizes implements getOutMsgQueueSizes operation. // // Get out msg queue sizes. diff --git a/pkg/oas/oas_unimplemented_gen.go b/pkg/oas/oas_unimplemented_gen.go index 6c62a46d..659e7a0d 100644 --- a/pkg/oas/oas_unimplemented_gen.go +++ b/pkg/oas/oas_unimplemented_gen.go @@ -5,6 +5,8 @@ package oas import ( "context" + "github.com/go-faster/jx" + ht "github.com/ogen-go/ogen/http" ) @@ -659,6 +661,24 @@ func (UnimplementedHandler) GetNftItemsByAddresses(ctx context.Context, req OptG return r, ht.ErrNotImplemented } +// GetOpenapiJson implements getOpenapiJson operation. +// +// Get the openapi.json file. +// +// GET /v2/openapi.json +func (UnimplementedHandler) GetOpenapiJson(ctx context.Context) (r jx.Raw, _ error) { + return r, ht.ErrNotImplemented +} + +// GetOpenapiYml implements getOpenapiYml operation. +// +// Get the openapi.yml file. +// +// GET /v2/openapi.yml +func (UnimplementedHandler) GetOpenapiYml(ctx context.Context) (r GetOpenapiYmlOK, _ error) { + return r, ht.ErrNotImplemented +} + // GetOutMsgQueueSizes implements getOutMsgQueueSizes operation. // // Get out msg queue sizes.