feat: generate OpenAPI server

This commit is contained in:
Bastien Riviere 2024-01-29 19:32:16 +01:00
parent 875a6d1a33
commit e17aa7dc38
Signed by: babariviere
GPG key ID: 4E5F0839249F162E
17 changed files with 1907 additions and 3 deletions

9
gen.go Normal file
View file

@ -0,0 +1,9 @@
//go:generate go run github.com/ogen-go/ogen/cmd/ogen --target internal/oas -package oas --clean openapi.yaml
//go:generate sqlc generate
package main
import (
// For go get ./...
_ "github.com/ogen-go/ogen"
)

281
internal/oas/oas_cfg_gen.go Normal file
View file

@ -0,0 +1,281 @@
// Code generated by ogen, DO NOT EDIT.
package oas
import (
"net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/trace"
ht "github.com/ogen-go/ogen/http"
"github.com/ogen-go/ogen/middleware"
"github.com/ogen-go/ogen/ogenerrors"
"github.com/ogen-go/ogen/otelogen"
)
var (
// Allocate option closure once.
clientSpanKind = trace.WithSpanKind(trace.SpanKindClient)
// Allocate option closure once.
serverSpanKind = trace.WithSpanKind(trace.SpanKindServer)
)
type (
optionFunc[C any] func(*C)
otelOptionFunc func(*otelConfig)
)
type otelConfig struct {
TracerProvider trace.TracerProvider
Tracer trace.Tracer
MeterProvider metric.MeterProvider
Meter metric.Meter
}
func (cfg *otelConfig) initOTEL() {
if cfg.TracerProvider == nil {
cfg.TracerProvider = otel.GetTracerProvider()
}
if cfg.MeterProvider == nil {
cfg.MeterProvider = otel.GetMeterProvider()
}
cfg.Tracer = cfg.TracerProvider.Tracer(otelogen.Name,
trace.WithInstrumentationVersion(otelogen.SemVersion()),
)
cfg.Meter = cfg.MeterProvider.Meter(otelogen.Name)
}
// ErrorHandler is error handler.
type ErrorHandler = ogenerrors.ErrorHandler
type serverConfig struct {
otelConfig
NotFound http.HandlerFunc
MethodNotAllowed func(w http.ResponseWriter, r *http.Request, allowed string)
ErrorHandler ErrorHandler
Prefix string
Middleware Middleware
MaxMultipartMemory int64
}
// ServerOption is server config option.
type ServerOption interface {
applyServer(*serverConfig)
}
var _ ServerOption = (optionFunc[serverConfig])(nil)
func (o optionFunc[C]) applyServer(c *C) {
o(c)
}
var _ ServerOption = (otelOptionFunc)(nil)
func (o otelOptionFunc) applyServer(c *serverConfig) {
o(&c.otelConfig)
}
func newServerConfig(opts ...ServerOption) serverConfig {
cfg := serverConfig{
NotFound: http.NotFound,
MethodNotAllowed: func(w http.ResponseWriter, r *http.Request, allowed string) {
status := http.StatusMethodNotAllowed
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Methods", allowed)
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
status = http.StatusNoContent
} else {
w.Header().Set("Allow", allowed)
}
w.WriteHeader(status)
},
ErrorHandler: ogenerrors.DefaultErrorHandler,
Middleware: nil,
MaxMultipartMemory: 32 << 20, // 32 MB
}
for _, opt := range opts {
opt.applyServer(&cfg)
}
cfg.initOTEL()
return cfg
}
type baseServer struct {
cfg serverConfig
requests metric.Int64Counter
errors metric.Int64Counter
duration metric.Float64Histogram
}
func (s baseServer) notFound(w http.ResponseWriter, r *http.Request) {
s.cfg.NotFound(w, r)
}
func (s baseServer) notAllowed(w http.ResponseWriter, r *http.Request, allowed string) {
s.cfg.MethodNotAllowed(w, r, allowed)
}
func (cfg serverConfig) baseServer() (s baseServer, err error) {
s = baseServer{cfg: cfg}
if s.requests, err = s.cfg.Meter.Int64Counter(otelogen.ServerRequestCount); err != nil {
return s, err
}
if s.errors, err = s.cfg.Meter.Int64Counter(otelogen.ServerErrorsCount); err != nil {
return s, err
}
if s.duration, err = s.cfg.Meter.Float64Histogram(otelogen.ServerDuration); err != nil {
return s, err
}
return s, nil
}
type clientConfig struct {
otelConfig
Client ht.Client
}
// ClientOption is client config option.
type ClientOption interface {
applyClient(*clientConfig)
}
var _ ClientOption = (optionFunc[clientConfig])(nil)
func (o optionFunc[C]) applyClient(c *C) {
o(c)
}
var _ ClientOption = (otelOptionFunc)(nil)
func (o otelOptionFunc) applyClient(c *clientConfig) {
o(&c.otelConfig)
}
func newClientConfig(opts ...ClientOption) clientConfig {
cfg := clientConfig{
Client: http.DefaultClient,
}
for _, opt := range opts {
opt.applyClient(&cfg)
}
cfg.initOTEL()
return cfg
}
type baseClient struct {
cfg clientConfig
requests metric.Int64Counter
errors metric.Int64Counter
duration metric.Float64Histogram
}
func (cfg clientConfig) baseClient() (c baseClient, err error) {
c = baseClient{cfg: cfg}
if c.requests, err = c.cfg.Meter.Int64Counter(otelogen.ClientRequestCount); err != nil {
return c, err
}
if c.errors, err = c.cfg.Meter.Int64Counter(otelogen.ClientErrorsCount); err != nil {
return c, err
}
if c.duration, err = c.cfg.Meter.Float64Histogram(otelogen.ClientDuration); err != nil {
return c, err
}
return c, nil
}
// Option is config option.
type Option interface {
ServerOption
ClientOption
}
// WithTracerProvider specifies a tracer provider to use for creating a tracer.
//
// If none is specified, the global provider is used.
func WithTracerProvider(provider trace.TracerProvider) Option {
return otelOptionFunc(func(cfg *otelConfig) {
if provider != nil {
cfg.TracerProvider = provider
}
})
}
// WithMeterProvider specifies a meter provider to use for creating a meter.
//
// If none is specified, the otel.GetMeterProvider() is used.
func WithMeterProvider(provider metric.MeterProvider) Option {
return otelOptionFunc(func(cfg *otelConfig) {
if provider != nil {
cfg.MeterProvider = provider
}
})
}
// WithClient specifies http client to use.
func WithClient(client ht.Client) ClientOption {
return optionFunc[clientConfig](func(cfg *clientConfig) {
if client != nil {
cfg.Client = client
}
})
}
// WithNotFound specifies Not Found handler to use.
func WithNotFound(notFound http.HandlerFunc) ServerOption {
return optionFunc[serverConfig](func(cfg *serverConfig) {
if notFound != nil {
cfg.NotFound = notFound
}
})
}
// WithMethodNotAllowed specifies Method Not Allowed handler to use.
func WithMethodNotAllowed(methodNotAllowed func(w http.ResponseWriter, r *http.Request, allowed string)) ServerOption {
return optionFunc[serverConfig](func(cfg *serverConfig) {
if methodNotAllowed != nil {
cfg.MethodNotAllowed = methodNotAllowed
}
})
}
// WithErrorHandler specifies error handler to use.
func WithErrorHandler(h ErrorHandler) ServerOption {
return optionFunc[serverConfig](func(cfg *serverConfig) {
if h != nil {
cfg.ErrorHandler = h
}
})
}
// WithPathPrefix specifies server path prefix.
func WithPathPrefix(prefix string) ServerOption {
return optionFunc[serverConfig](func(cfg *serverConfig) {
cfg.Prefix = prefix
})
}
// WithMiddleware specifies middlewares to use.
func WithMiddleware(m ...Middleware) ServerOption {
return optionFunc[serverConfig](func(cfg *serverConfig) {
switch len(m) {
case 0:
cfg.Middleware = nil
case 1:
cfg.Middleware = m[0]
default:
cfg.Middleware = middleware.ChainMiddlewares(m...)
}
})
}
// WithMaxMultipartMemory specifies limit of memory for storing file parts.
// File parts which can't be stored in memory will be stored on disk in temporary files.
func WithMaxMultipartMemory(max int64) ServerOption {
return optionFunc[serverConfig](func(cfg *serverConfig) {
if max > 0 {
cfg.MaxMultipartMemory = max
}
})
}

View file

@ -0,0 +1,244 @@
// Code generated by ogen, DO NOT EDIT.
package oas
import (
"context"
"net/url"
"strings"
"time"
"github.com/go-faster/errors"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/metric"
semconv "go.opentelemetry.io/otel/semconv/v1.19.0"
"go.opentelemetry.io/otel/trace"
"github.com/ogen-go/ogen/conv"
ht "github.com/ogen-go/ogen/http"
"github.com/ogen-go/ogen/uri"
)
// Invoker invokes operations described by OpenAPI v3 specification.
type Invoker interface {
// CreatePost invokes POST /create operation.
//
// POST /create
CreatePost(ctx context.Context, request *CreatePostReq) (CreatePostRes, error)
// HashGet invokes GET /{hash} operation.
//
// Redirect client to long URL.
//
// GET /{hash}
HashGet(ctx context.Context, params HashGetParams) (HashGetRes, error)
}
// Client implements OAS client.
type Client struct {
serverURL *url.URL
baseClient
}
var _ Handler = struct {
*Client
}{}
func trimTrailingSlashes(u *url.URL) {
u.Path = strings.TrimRight(u.Path, "/")
u.RawPath = strings.TrimRight(u.RawPath, "/")
}
// NewClient initializes new Client defined by OAS.
func NewClient(serverURL string, opts ...ClientOption) (*Client, error) {
u, err := url.Parse(serverURL)
if err != nil {
return nil, err
}
trimTrailingSlashes(u)
c, err := newClientConfig(opts...).baseClient()
if err != nil {
return nil, err
}
return &Client{
serverURL: u,
baseClient: c,
}, nil
}
type serverURLKey struct{}
// WithServerURL sets context key to override server URL.
func WithServerURL(ctx context.Context, u *url.URL) context.Context {
return context.WithValue(ctx, serverURLKey{}, u)
}
func (c *Client) requestURL(ctx context.Context) *url.URL {
u, ok := ctx.Value(serverURLKey{}).(*url.URL)
if !ok {
return c.serverURL
}
return u
}
// CreatePost invokes POST /create operation.
//
// POST /create
func (c *Client) CreatePost(ctx context.Context, request *CreatePostReq) (CreatePostRes, error) {
res, err := c.sendCreatePost(ctx, request)
return res, err
}
func (c *Client) sendCreatePost(ctx context.Context, request *CreatePostReq) (res CreatePostRes, err error) {
otelAttrs := []attribute.KeyValue{
semconv.HTTPMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/create"),
}
// Run stopwatch.
startTime := time.Now()
defer func() {
// Use floating point division here for higher precision (instead of Millisecond method).
elapsedDuration := time.Since(startTime)
c.duration.Record(ctx, float64(float64(elapsedDuration)/float64(time.Millisecond)), metric.WithAttributes(otelAttrs...))
}()
// Increment request counter.
c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...))
// Start a span for this request.
ctx, span := c.cfg.Tracer.Start(ctx, "CreatePost",
trace.WithAttributes(otelAttrs...),
clientSpanKind,
)
// Track stage for error reporting.
var stage string
defer func() {
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, stage)
c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...))
}
span.End()
}()
stage = "BuildURL"
u := uri.Clone(c.requestURL(ctx))
var pathParts [1]string
pathParts[0] = "/create"
uri.AddPathParts(u, pathParts[:]...)
stage = "EncodeRequest"
r, err := ht.NewRequest(ctx, "POST", u)
if err != nil {
return res, errors.Wrap(err, "create request")
}
if err := encodeCreatePostRequest(request, r); err != nil {
return res, errors.Wrap(err, "encode request")
}
stage = "SendRequest"
resp, err := c.cfg.Client.Do(r)
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
stage = "DecodeResponse"
result, err := decodeCreatePostResponse(resp)
if err != nil {
return res, errors.Wrap(err, "decode response")
}
return result, nil
}
// HashGet invokes GET /{hash} operation.
//
// Redirect client to long URL.
//
// GET /{hash}
func (c *Client) HashGet(ctx context.Context, params HashGetParams) (HashGetRes, error) {
res, err := c.sendHashGet(ctx, params)
return res, err
}
func (c *Client) sendHashGet(ctx context.Context, params HashGetParams) (res HashGetRes, err error) {
otelAttrs := []attribute.KeyValue{
semconv.HTTPMethodKey.String("GET"),
semconv.HTTPRouteKey.String("/{hash}"),
}
// Run stopwatch.
startTime := time.Now()
defer func() {
// Use floating point division here for higher precision (instead of Millisecond method).
elapsedDuration := time.Since(startTime)
c.duration.Record(ctx, float64(float64(elapsedDuration)/float64(time.Millisecond)), metric.WithAttributes(otelAttrs...))
}()
// Increment request counter.
c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...))
// Start a span for this request.
ctx, span := c.cfg.Tracer.Start(ctx, "HashGet",
trace.WithAttributes(otelAttrs...),
clientSpanKind,
)
// Track stage for error reporting.
var stage string
defer func() {
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, stage)
c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...))
}
span.End()
}()
stage = "BuildURL"
u := uri.Clone(c.requestURL(ctx))
var pathParts [2]string
pathParts[0] = "/"
{
// Encode "hash" parameter.
e := uri.NewPathEncoder(uri.PathEncoderConfig{
Param: "hash",
Style: uri.PathStyleSimple,
Explode: false,
})
if err := func() error {
return e.EncodeValue(conv.StringToString(params.Hash))
}(); err != nil {
return res, errors.Wrap(err, "encode path")
}
encoded, err := e.Result()
if err != nil {
return res, errors.Wrap(err, "encode path")
}
pathParts[1] = encoded
}
uri.AddPathParts(u, pathParts[:]...)
stage = "EncodeRequest"
r, err := ht.NewRequest(ctx, "GET", u)
if err != nil {
return res, errors.Wrap(err, "create request")
}
stage = "SendRequest"
resp, err := c.cfg.Client.Do(r)
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
stage = "DecodeResponse"
result, err := decodeHashGetResponse(resp)
if err != nil {
return res, errors.Wrap(err, "decode response")
}
return result, nil
}

View file

@ -0,0 +1,228 @@
// Code generated by ogen, DO NOT EDIT.
package oas
import (
"context"
"net/http"
"time"
"github.com/go-faster/errors"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/metric"
semconv "go.opentelemetry.io/otel/semconv/v1.19.0"
"go.opentelemetry.io/otel/trace"
ht "github.com/ogen-go/ogen/http"
"github.com/ogen-go/ogen/middleware"
"github.com/ogen-go/ogen/ogenerrors"
)
// handleCreatePostRequest handles POST /create operation.
//
// POST /create
func (s *Server) handleCreatePostRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) {
otelAttrs := []attribute.KeyValue{
semconv.HTTPMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/create"),
}
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), "CreatePost",
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
opErrContext = ogenerrors.OperationContext{
Name: "CreatePost",
ID: "",
}
)
request, close, err := s.decodeCreatePostRequest(r)
if err != nil {
err = &ogenerrors.DecodeRequestError{
OperationContext: opErrContext,
Err: err,
}
recordError("DecodeRequest", err)
s.cfg.ErrorHandler(ctx, w, r, err)
return
}
defer func() {
if err := close(); err != nil {
recordError("CloseRequest", err)
}
}()
var response CreatePostRes
if m := s.cfg.Middleware; m != nil {
mreq := middleware.Request{
Context: ctx,
OperationName: "CreatePost",
OperationSummary: "",
OperationID: "",
Body: request,
Params: middleware.Parameters{},
Raw: r,
}
type (
Request = *CreatePostReq
Params = struct{}
Response = CreatePostRes
)
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.CreatePost(ctx, request)
return response, err
},
)
} else {
response, err = s.h.CreatePost(ctx, request)
}
if err != nil {
recordError("Internal", err)
s.cfg.ErrorHandler(ctx, w, r, err)
return
}
if err := encodeCreatePostResponse(response, w, span); err != nil {
recordError("EncodeResponse", err)
if !errors.Is(err, ht.ErrInternalServerErrorResponse) {
s.cfg.ErrorHandler(ctx, w, r, err)
}
return
}
}
// handleHashGetRequest handles GET /{hash} operation.
//
// Redirect client to long URL.
//
// GET /{hash}
func (s *Server) handleHashGetRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) {
otelAttrs := []attribute.KeyValue{
semconv.HTTPMethodKey.String("GET"),
semconv.HTTPRouteKey.String("/{hash}"),
}
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), "HashGet",
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
opErrContext = ogenerrors.OperationContext{
Name: "HashGet",
ID: "",
}
)
params, err := decodeHashGetParams(args, argsEscaped, r)
if err != nil {
err = &ogenerrors.DecodeParamsError{
OperationContext: opErrContext,
Err: err,
}
recordError("DecodeParams", err)
s.cfg.ErrorHandler(ctx, w, r, err)
return
}
var response HashGetRes
if m := s.cfg.Middleware; m != nil {
mreq := middleware.Request{
Context: ctx,
OperationName: "HashGet",
OperationSummary: "",
OperationID: "",
Body: nil,
Params: middleware.Parameters{
{
Name: "hash",
In: "path",
}: params.Hash,
},
Raw: r,
}
type (
Request = struct{}
Params = HashGetParams
Response = HashGetRes
)
response, err = middleware.HookMiddleware[
Request,
Params,
Response,
](
m,
mreq,
unpackHashGetParams,
func(ctx context.Context, request Request, params Params) (response Response, err error) {
response, err = s.h.HashGet(ctx, params)
return response, err
},
)
} else {
response, err = s.h.HashGet(ctx, params)
}
if err != nil {
recordError("Internal", err)
s.cfg.ErrorHandler(ctx, w, r, err)
return
}
if err := encodeHashGetResponse(response, w, span); err != nil {
recordError("EncodeResponse", err)
if !errors.Is(err, ht.ErrInternalServerErrorResponse) {
s.cfg.ErrorHandler(ctx, w, r, err)
}
return
}
}

View file

@ -0,0 +1,10 @@
// Code generated by ogen, DO NOT EDIT.
package oas
type CreatePostRes interface {
createPostRes()
}
type HashGetRes interface {
hashGetRes()
}

View file

@ -0,0 +1,270 @@
// Code generated by ogen, DO NOT EDIT.
package oas
import (
"math/bits"
"strconv"
"github.com/go-faster/errors"
"github.com/go-faster/jx"
"github.com/ogen-go/ogen/validate"
)
// Encode implements json.Marshaler.
func (s *CreatePostBadRequest) Encode(e *jx.Encoder) {
e.ObjStart()
s.encodeFields(e)
e.ObjEnd()
}
// encodeFields encodes fields.
func (s *CreatePostBadRequest) encodeFields(e *jx.Encoder) {
{
if s.Message.Set {
e.FieldStart("message")
s.Message.Encode(e)
}
}
}
var jsonFieldsNameOfCreatePostBadRequest = [1]string{
0: "message",
}
// Decode decodes CreatePostBadRequest from json.
func (s *CreatePostBadRequest) Decode(d *jx.Decoder) error {
if s == nil {
return errors.New("invalid: unable to decode CreatePostBadRequest to nil")
}
if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error {
switch string(k) {
case "message":
if err := func() error {
s.Message.Reset()
if err := s.Message.Decode(d); err != nil {
return err
}
return nil
}(); err != nil {
return errors.Wrap(err, "decode field \"message\"")
}
default:
return d.Skip()
}
return nil
}); err != nil {
return errors.Wrap(err, "decode CreatePostBadRequest")
}
return nil
}
// MarshalJSON implements stdjson.Marshaler.
func (s *CreatePostBadRequest) MarshalJSON() ([]byte, error) {
e := jx.Encoder{}
s.Encode(&e)
return e.Bytes(), nil
}
// UnmarshalJSON implements stdjson.Unmarshaler.
func (s *CreatePostBadRequest) UnmarshalJSON(data []byte) error {
d := jx.DecodeBytes(data)
return s.Decode(d)
}
// Encode implements json.Marshaler.
func (s *CreatePostCreated) Encode(e *jx.Encoder) {
e.ObjStart()
s.encodeFields(e)
e.ObjEnd()
}
// encodeFields encodes fields.
func (s *CreatePostCreated) encodeFields(e *jx.Encoder) {
{
if s.Shorten.Set {
e.FieldStart("shorten")
s.Shorten.Encode(e)
}
}
}
var jsonFieldsNameOfCreatePostCreated = [1]string{
0: "shorten",
}
// Decode decodes CreatePostCreated from json.
func (s *CreatePostCreated) Decode(d *jx.Decoder) error {
if s == nil {
return errors.New("invalid: unable to decode CreatePostCreated to nil")
}
if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error {
switch string(k) {
case "shorten":
if err := func() error {
s.Shorten.Reset()
if err := s.Shorten.Decode(d); err != nil {
return err
}
return nil
}(); err != nil {
return errors.Wrap(err, "decode field \"shorten\"")
}
default:
return d.Skip()
}
return nil
}); err != nil {
return errors.Wrap(err, "decode CreatePostCreated")
}
return nil
}
// MarshalJSON implements stdjson.Marshaler.
func (s *CreatePostCreated) MarshalJSON() ([]byte, error) {
e := jx.Encoder{}
s.Encode(&e)
return e.Bytes(), nil
}
// UnmarshalJSON implements stdjson.Unmarshaler.
func (s *CreatePostCreated) UnmarshalJSON(data []byte) error {
d := jx.DecodeBytes(data)
return s.Decode(d)
}
// Encode implements json.Marshaler.
func (s *CreatePostReq) Encode(e *jx.Encoder) {
e.ObjStart()
s.encodeFields(e)
e.ObjEnd()
}
// encodeFields encodes fields.
func (s *CreatePostReq) encodeFields(e *jx.Encoder) {
{
e.FieldStart("url")
e.Str(s.URL)
}
}
var jsonFieldsNameOfCreatePostReq = [1]string{
0: "url",
}
// Decode decodes CreatePostReq from json.
func (s *CreatePostReq) Decode(d *jx.Decoder) error {
if s == nil {
return errors.New("invalid: unable to decode CreatePostReq to nil")
}
var requiredBitSet [1]uint8
if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error {
switch string(k) {
case "url":
requiredBitSet[0] |= 1 << 0
if err := func() error {
v, err := d.Str()
s.URL = string(v)
if err != nil {
return err
}
return nil
}(); err != nil {
return errors.Wrap(err, "decode field \"url\"")
}
default:
return d.Skip()
}
return nil
}); err != nil {
return errors.Wrap(err, "decode CreatePostReq")
}
// Validate required fields.
var failures []validate.FieldError
for i, mask := range [1]uint8{
0b00000001,
} {
if result := (requiredBitSet[i] & mask) ^ mask; result != 0 {
// Mask only required fields and check equality to mask using XOR.
//
// If XOR result is not zero, result is not equal to expected, so some fields are missed.
// Bits of fields which would be set are actually bits of missed fields.
missed := bits.OnesCount8(result)
for bitN := 0; bitN < missed; bitN++ {
bitIdx := bits.TrailingZeros8(result)
fieldIdx := i*8 + bitIdx
var name string
if fieldIdx < len(jsonFieldsNameOfCreatePostReq) {
name = jsonFieldsNameOfCreatePostReq[fieldIdx]
} else {
name = strconv.Itoa(fieldIdx)
}
failures = append(failures, validate.FieldError{
Name: name,
Error: validate.ErrFieldRequired,
})
// Reset bit.
result &^= 1 << bitIdx
}
}
}
if len(failures) > 0 {
return &validate.Error{Fields: failures}
}
return nil
}
// MarshalJSON implements stdjson.Marshaler.
func (s *CreatePostReq) MarshalJSON() ([]byte, error) {
e := jx.Encoder{}
s.Encode(&e)
return e.Bytes(), nil
}
// UnmarshalJSON implements stdjson.Unmarshaler.
func (s *CreatePostReq) UnmarshalJSON(data []byte) error {
d := jx.DecodeBytes(data)
return s.Decode(d)
}
// Encode encodes string as json.
func (o OptString) Encode(e *jx.Encoder) {
if !o.Set {
return
}
e.Str(string(o.Value))
}
// Decode decodes string from json.
func (o *OptString) Decode(d *jx.Decoder) error {
if o == nil {
return errors.New("invalid: unable to decode OptString to nil")
}
o.Set = true
v, err := d.Str()
if err != nil {
return err
}
o.Value = string(v)
return nil
}
// MarshalJSON implements stdjson.Marshaler.
func (s OptString) MarshalJSON() ([]byte, error) {
e := jx.Encoder{}
s.Encode(&e)
return e.Bytes(), nil
}
// UnmarshalJSON implements stdjson.Unmarshaler.
func (s *OptString) UnmarshalJSON(data []byte) error {
d := jx.DecodeBytes(data)
return s.Decode(d)
}

View file

@ -0,0 +1,10 @@
// Code generated by ogen, DO NOT EDIT.
package oas
import (
"github.com/ogen-go/ogen/middleware"
)
// Middleware is middleware type.
type Middleware = middleware.Middleware

View file

@ -0,0 +1,82 @@
// Code generated by ogen, DO NOT EDIT.
package oas
import (
"net/http"
"net/url"
"github.com/go-faster/errors"
"github.com/ogen-go/ogen/conv"
"github.com/ogen-go/ogen/middleware"
"github.com/ogen-go/ogen/ogenerrors"
"github.com/ogen-go/ogen/uri"
"github.com/ogen-go/ogen/validate"
)
// HashGetParams is parameters of GET /{hash} operation.
type HashGetParams struct {
// Hash of shorten URL.
Hash string
}
func unpackHashGetParams(packed middleware.Parameters) (params HashGetParams) {
{
key := middleware.ParameterKey{
Name: "hash",
In: "path",
}
params.Hash = packed[key].(string)
}
return params
}
func decodeHashGetParams(args [1]string, argsEscaped bool, r *http.Request) (params HashGetParams, _ error) {
// Decode path: hash.
if err := func() error {
param := args[0]
if argsEscaped {
unescaped, err := url.PathUnescape(args[0])
if err != nil {
return errors.Wrap(err, "unescape path")
}
param = unescaped
}
if len(param) > 0 {
d := uri.NewPathDecoder(uri.PathDecoderConfig{
Param: "hash",
Value: param,
Style: uri.PathStyleSimple,
Explode: false,
})
if err := func() error {
val, err := d.DecodeValue()
if err != nil {
return err
}
c, err := conv.ToString(val)
if err != nil {
return err
}
params.Hash = c
return nil
}(); err != nil {
return err
}
} else {
return validate.ErrFieldRequired
}
return nil
}(); err != nil {
return params, &ogenerrors.DecodeParamError{
Name: "hash",
In: "path",
Err: err,
}
}
return params, nil
}

View file

@ -0,0 +1,79 @@
// Code generated by ogen, DO NOT EDIT.
package oas
import (
"io"
"mime"
"net/http"
"github.com/go-faster/errors"
"github.com/go-faster/jx"
"go.uber.org/multierr"
"github.com/ogen-go/ogen/ogenerrors"
"github.com/ogen-go/ogen/validate"
)
func (s *Server) decodeCreatePostRequest(r *http.Request) (
req *CreatePostReq,
close func() error,
rerr error,
) {
var closers []func() error
close = func() error {
var merr error
// Close in reverse order, to match defer behavior.
for i := len(closers) - 1; i >= 0; i-- {
c := closers[i]
merr = multierr.Append(merr, c())
}
return merr
}
defer func() {
if rerr != nil {
rerr = multierr.Append(rerr, close())
}
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
if err != nil {
return req, close, err
}
if len(buf) == 0 {
return req, close, validate.ErrBodyRequired
}
d := jx.DecodeBytes(buf)
var request CreatePostReq
if err := func() error {
if err := request.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return req, close, err
}
return &request, close, nil
default:
return req, close, validate.InvalidContentType(ct)
}
}

View file

@ -0,0 +1,26 @@
// Code generated by ogen, DO NOT EDIT.
package oas
import (
"bytes"
"net/http"
"github.com/go-faster/jx"
ht "github.com/ogen-go/ogen/http"
)
func encodeCreatePostRequest(
req *CreatePostReq,
r *http.Request,
) error {
const contentType = "application/json"
e := new(jx.Encoder)
{
req.Encode(e)
}
encoded := e.Bytes()
ht.SetBody(r, bytes.NewReader(encoded), contentType)
return nil
}

View file

@ -0,0 +1,144 @@
// Code generated by ogen, DO NOT EDIT.
package oas
import (
"io"
"mime"
"net/http"
"github.com/go-faster/errors"
"github.com/go-faster/jx"
"github.com/ogen-go/ogen/conv"
"github.com/ogen-go/ogen/ogenerrors"
"github.com/ogen-go/ogen/uri"
"github.com/ogen-go/ogen/validate"
)
func decodeCreatePostResponse(resp *http.Response) (res CreatePostRes, _ error) {
switch resp.StatusCode {
case 201:
// Code 201.
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response CreatePostCreated
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
return &response, nil
default:
return res, validate.InvalidContentType(ct)
}
case 400:
// Code 400.
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response CreatePostBadRequest
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
return &response, nil
default:
return res, validate.InvalidContentType(ct)
}
}
return res, validate.UnexpectedStatusCode(resp.StatusCode)
}
func decodeHashGetResponse(resp *http.Response) (res HashGetRes, _ error) {
switch resp.StatusCode {
case 307:
// Code 307.
var wrapper HashGetTemporaryRedirect
h := uri.NewHeaderDecoder(resp.Header)
// Parse "Location" header.
{
cfg := uri.HeaderParameterDecodingConfig{
Name: "Location",
Explode: false,
}
if err := func() error {
if err := h.HasParam(cfg); err == nil {
if err := h.DecodeParam(cfg, func(d uri.Decoder) error {
var wrapperDotLocationVal string
if err := func() error {
val, err := d.DecodeValue()
if err != nil {
return err
}
c, err := conv.ToString(val)
if err != nil {
return err
}
wrapperDotLocationVal = c
return nil
}(); err != nil {
return err
}
wrapper.Location.SetTo(wrapperDotLocationVal)
return nil
}); err != nil {
return err
}
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "parse Location header")
}
}
return &wrapper, nil
case 404:
// Code 404.
return &HashGetNotFound{}, nil
}
return res, validate.UnexpectedStatusCode(resp.StatusCode)
}

View file

@ -0,0 +1,86 @@
// Code generated by ogen, DO NOT EDIT.
package oas
import (
"net/http"
"github.com/go-faster/errors"
"github.com/go-faster/jx"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"github.com/ogen-go/ogen/conv"
"github.com/ogen-go/ogen/uri"
)
func encodeCreatePostResponse(response CreatePostRes, w http.ResponseWriter, span trace.Span) error {
switch response := response.(type) {
case *CreatePostCreated:
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(201)
span.SetStatus(codes.Ok, http.StatusText(201))
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
case *CreatePostBadRequest:
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(400)
span.SetStatus(codes.Error, http.StatusText(400))
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
default:
return errors.Errorf("unexpected response type: %T", response)
}
}
func encodeHashGetResponse(response HashGetRes, w http.ResponseWriter, span trace.Span) error {
switch response := response.(type) {
case *HashGetTemporaryRedirect:
// Encoding response headers.
{
h := uri.NewHeaderEncoder(w.Header())
// Encode "Location" header.
{
cfg := uri.HeaderParameterEncodingConfig{
Name: "Location",
Explode: false,
}
if err := h.EncodeParam(cfg, func(e uri.Encoder) error {
if val, ok := response.Location.Get(); ok {
return e.EncodeValue(conv.StringToString(val))
}
return nil
}); err != nil {
return errors.Wrap(err, "encode Location header")
}
}
}
w.WriteHeader(307)
span.SetStatus(codes.Ok, http.StatusText(307))
return nil
case *HashGetNotFound:
w.WriteHeader(404)
span.SetStatus(codes.Error, http.StatusText(404))
return nil
default:
return errors.Errorf("unexpected response type: %T", response)
}
}

View file

@ -0,0 +1,249 @@
// Code generated by ogen, DO NOT EDIT.
package oas
import (
"net/http"
"net/url"
"strings"
"github.com/ogen-go/ogen/uri"
)
func (s *Server) cutPrefix(path string) (string, bool) {
prefix := s.cfg.Prefix
if prefix == "" {
return path, true
}
if !strings.HasPrefix(path, prefix) {
// Prefix doesn't match.
return "", false
}
// Cut prefix from the path.
return strings.TrimPrefix(path, prefix), true
}
// ServeHTTP serves http request as defined by OpenAPI v3 specification,
// calling handler that matches the path or returning not found error.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
elem := r.URL.Path
elemIsEscaped := false
if rawPath := r.URL.RawPath; rawPath != "" {
if normalized, ok := uri.NormalizeEscapedPath(rawPath); ok {
elem = normalized
elemIsEscaped = strings.ContainsRune(elem, '%')
}
}
elem, ok := s.cutPrefix(elem)
if !ok || len(elem) == 0 {
s.notFound(w, r)
return
}
args := [1]string{}
// Static code generated router with unwrapped path search.
switch {
default:
if len(elem) == 0 {
break
}
switch elem[0] {
case '/': // Prefix: "/"
origElem := elem
if l := len("/"); len(elem) >= l && elem[0:l] == "/" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
break
}
switch elem[0] {
case 'c': // Prefix: "create"
origElem := elem
if l := len("create"); len(elem) >= l && elem[0:l] == "create" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
// Leaf node.
switch r.Method {
case "POST":
s.handleCreatePostRequest([0]string{}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
}
return
}
elem = origElem
}
// Param: "hash"
// Leaf parameter
args[0] = elem
elem = ""
if len(elem) == 0 {
// Leaf node.
switch r.Method {
case "GET":
s.handleHashGetRequest([1]string{
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "GET")
}
return
}
elem = origElem
}
}
s.notFound(w, r)
}
// Route is route object.
type Route struct {
name string
summary string
operationID string
pathPattern string
count int
args [1]string
}
// Name returns ogen operation name.
//
// It is guaranteed to be unique and not empty.
func (r Route) Name() string {
return r.name
}
// Summary returns OpenAPI summary.
func (r Route) Summary() string {
return r.summary
}
// OperationID returns OpenAPI operationId.
func (r Route) OperationID() string {
return r.operationID
}
// PathPattern returns OpenAPI path.
func (r Route) PathPattern() string {
return r.pathPattern
}
// Args returns parsed arguments.
func (r Route) Args() []string {
return r.args[:r.count]
}
// FindRoute finds Route for given method and path.
//
// Note: this method does not unescape path or handle reserved characters in path properly. Use FindPath instead.
func (s *Server) FindRoute(method, path string) (Route, bool) {
return s.FindPath(method, &url.URL{Path: path})
}
// FindPath finds Route for given method and URL.
func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
var (
elem = u.Path
args = r.args
)
if rawPath := u.RawPath; rawPath != "" {
if normalized, ok := uri.NormalizeEscapedPath(rawPath); ok {
elem = normalized
}
defer func() {
for i, arg := range r.args[:r.count] {
if unescaped, err := url.PathUnescape(arg); err == nil {
r.args[i] = unescaped
}
}
}()
}
elem, ok := s.cutPrefix(elem)
if !ok {
return r, false
}
// Static code generated router with unwrapped path search.
switch {
default:
if len(elem) == 0 {
break
}
switch elem[0] {
case '/': // Prefix: "/"
origElem := elem
if l := len("/"); len(elem) >= l && elem[0:l] == "/" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
break
}
switch elem[0] {
case 'c': // Prefix: "create"
origElem := elem
if l := len("create"); len(elem) >= l && elem[0:l] == "create" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
switch method {
case "POST":
// Leaf: CreatePost
r.name = "CreatePost"
r.summary = ""
r.operationID = ""
r.pathPattern = "/create"
r.args = args
r.count = 0
return r, true
default:
return
}
}
elem = origElem
}
// Param: "hash"
// Leaf parameter
args[0] = elem
elem = ""
if len(elem) == 0 {
switch method {
case "GET":
// Leaf: HashGet
r.name = "HashGet"
r.summary = ""
r.operationID = ""
r.pathPattern = "/{hash}"
r.args = args
r.count = 1
return r, true
default:
return
}
}
elem = origElem
}
}
return r, false
}

View file

@ -0,0 +1,119 @@
// Code generated by ogen, DO NOT EDIT.
package oas
type CreatePostBadRequest struct {
Message OptString `json:"message"`
}
// GetMessage returns the value of Message.
func (s *CreatePostBadRequest) GetMessage() OptString {
return s.Message
}
// SetMessage sets the value of Message.
func (s *CreatePostBadRequest) SetMessage(val OptString) {
s.Message = val
}
func (*CreatePostBadRequest) createPostRes() {}
type CreatePostCreated struct {
// Created shorten URL. Going to this URL should redirect to URL from request body.
Shorten OptString `json:"shorten"`
}
// GetShorten returns the value of Shorten.
func (s *CreatePostCreated) GetShorten() OptString {
return s.Shorten
}
// SetShorten sets the value of Shorten.
func (s *CreatePostCreated) SetShorten(val OptString) {
s.Shorten = val
}
func (*CreatePostCreated) createPostRes() {}
type CreatePostReq struct {
// URL to shorten.
URL string `json:"url"`
}
// GetURL returns the value of URL.
func (s *CreatePostReq) GetURL() string {
return s.URL
}
// SetURL sets the value of URL.
func (s *CreatePostReq) SetURL(val string) {
s.URL = val
}
// HashGetNotFound is response for HashGet operation.
type HashGetNotFound struct{}
func (*HashGetNotFound) hashGetRes() {}
// HashGetTemporaryRedirect is response for HashGet operation.
type HashGetTemporaryRedirect struct {
Location OptString
}
// GetLocation returns the value of Location.
func (s *HashGetTemporaryRedirect) GetLocation() OptString {
return s.Location
}
// SetLocation sets the value of Location.
func (s *HashGetTemporaryRedirect) SetLocation(val OptString) {
s.Location = val
}
func (*HashGetTemporaryRedirect) hashGetRes() {}
// NewOptString returns new OptString with value set to v.
func NewOptString(v string) OptString {
return OptString{
Value: v,
Set: true,
}
}
// OptString is optional string.
type OptString struct {
Value string
Set bool
}
// IsSet returns true if OptString was set.
func (o OptString) IsSet() bool { return o.Set }
// Reset unsets value.
func (o *OptString) Reset() {
var v string
o.Value = v
o.Set = false
}
// SetTo sets value to v.
func (o *OptString) SetTo(v string) {
o.Set = true
o.Value = v
}
// Get returns value and boolean that denotes whether value was set.
func (o OptString) Get() (v string, ok bool) {
if !o.Set {
return v, false
}
return o.Value, true
}
// Or returns value if set, or given parameter if does not.
func (o OptString) Or(d string) string {
if v, ok := o.Get(); ok {
return v
}
return d
}

View file

@ -0,0 +1,40 @@
// Code generated by ogen, DO NOT EDIT.
package oas
import (
"context"
)
// Handler handles operations described by OpenAPI v3 specification.
type Handler interface {
// CreatePost implements POST /create operation.
//
// POST /create
CreatePost(ctx context.Context, req *CreatePostReq) (CreatePostRes, error)
// HashGet implements GET /{hash} operation.
//
// Redirect client to long URL.
//
// GET /{hash}
HashGet(ctx context.Context, params HashGetParams) (HashGetRes, error)
}
// Server implements http server based on OpenAPI v3 specification and
// calls Handler to handle requests.
type Server struct {
h Handler
baseServer
}
// NewServer creates new Server.
func NewServer(h Handler, opts ...ServerOption) (*Server, error) {
s, err := newServerConfig(opts...).baseServer()
if err != nil {
return nil, err
}
return &Server{
h: h,
baseServer: s,
}, nil
}

View file

@ -0,0 +1,30 @@
// Code generated by ogen, DO NOT EDIT.
package oas
import (
"context"
ht "github.com/ogen-go/ogen/http"
)
// UnimplementedHandler is no-op Handler which returns http.ErrNotImplemented.
type UnimplementedHandler struct{}
var _ Handler = UnimplementedHandler{}
// CreatePost implements POST /create operation.
//
// POST /create
func (UnimplementedHandler) CreatePost(ctx context.Context, req *CreatePostReq) (r CreatePostRes, _ error) {
return r, ht.ErrNotImplemented
}
// HashGet implements GET /{hash} operation.
//
// Redirect client to long URL.
//
// GET /{hash}
func (UnimplementedHandler) HashGet(ctx context.Context, params HashGetParams) (r HashGetRes, _ error) {
return r, ht.ErrNotImplemented
}

View file

@ -51,7 +51,6 @@ paths:
shorten:
type: string
description: Created shorten URL. Going to this URL should redirect to URL from request body.
examples: {}
"400":
description: Bad Request
content:
@ -65,5 +64,3 @@ paths:
Example 1:
value:
message: URL already exist.
components:
schemas: {}