Improve API

This commit is contained in:
2023-04-02 17:31:37 +03:00
parent bc61b916f9
commit 11a6171954
12 changed files with 260 additions and 25 deletions

View File

@@ -62,6 +62,22 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/page' $ref: '#/components/schemas/page'
400:
description: Bad request
content:
application/json:
schema:
type: object
properties:
field:
type: string
nullable: false
error:
type: string
nullable: false
required:
- error
- field
default: default:
$ref: '#/components/responses/undefinedError' $ref: '#/components/responses/undefinedError'

View File

@@ -76,13 +76,13 @@ func (c *Client) requestURL(ctx context.Context) *url.URL {
// Add new page. // Add new page.
// //
// POST /pages // POST /pages
func (c *Client) AddPage(ctx context.Context, request OptAddPageReq, params AddPageParams) (*Page, error) { func (c *Client) AddPage(ctx context.Context, request OptAddPageReq, params AddPageParams) (AddPageRes, error) {
res, err := c.sendAddPage(ctx, request, params) res, err := c.sendAddPage(ctx, request, params)
_ = res _ = res
return res, err return res, err
} }
func (c *Client) sendAddPage(ctx context.Context, request OptAddPageReq, params AddPageParams) (res *Page, err error) { func (c *Client) sendAddPage(ctx context.Context, request OptAddPageReq, params AddPageParams) (res AddPageRes, err error) {
otelAttrs := []attribute.KeyValue{ otelAttrs := []attribute.KeyValue{
otelogen.OperationID("addPage"), otelogen.OperationID("addPage"),
} }

View File

@@ -86,7 +86,7 @@ func (s *Server) handleAddPageRequest(args [0]string, argsEscaped bool, w http.R
} }
}() }()
var response *Page var response AddPageRes
if m := s.cfg.Middleware; m != nil { if m := s.cfg.Middleware; m != nil {
mreq := middleware.Request{ mreq := middleware.Request{
Context: ctx, Context: ctx,
@@ -113,7 +113,7 @@ func (s *Server) handleAddPageRequest(args [0]string, argsEscaped bool, w http.R
type ( type (
Request = OptAddPageReq Request = OptAddPageReq
Params = AddPageParams Params = AddPageParams
Response = *Page Response = AddPageRes
) )
response, err = middleware.HookMiddleware[ response, err = middleware.HookMiddleware[
Request, Request,

View File

@@ -1,6 +1,10 @@
// Code generated by ogen, DO NOT EDIT. // Code generated by ogen, DO NOT EDIT.
package openapi package openapi
type AddPageRes interface {
addPageRes()
}
type GetFileRes interface { type GetFileRes interface {
getFileRes() getFileRes()
} }

View File

@@ -13,6 +13,121 @@ import (
"github.com/ogen-go/ogen/validate" "github.com/ogen-go/ogen/validate"
) )
// Encode implements json.Marshaler.
func (s *AddPageBadRequest) Encode(e *jx.Encoder) {
e.ObjStart()
s.encodeFields(e)
e.ObjEnd()
}
// encodeFields encodes fields.
func (s *AddPageBadRequest) encodeFields(e *jx.Encoder) {
{
e.FieldStart("field")
e.Str(s.Field)
}
{
e.FieldStart("error")
e.Str(s.Error)
}
}
var jsonFieldsNameOfAddPageBadRequest = [2]string{
0: "field",
1: "error",
}
// Decode decodes AddPageBadRequest from json.
func (s *AddPageBadRequest) Decode(d *jx.Decoder) error {
if s == nil {
return errors.New("invalid: unable to decode AddPageBadRequest to nil")
}
var requiredBitSet [1]uint8
if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error {
switch string(k) {
case "field":
requiredBitSet[0] |= 1 << 0
if err := func() error {
v, err := d.Str()
s.Field = string(v)
if err != nil {
return err
}
return nil
}(); err != nil {
return errors.Wrap(err, "decode field \"field\"")
}
case "error":
requiredBitSet[0] |= 1 << 1
if err := func() error {
v, err := d.Str()
s.Error = string(v)
if err != nil {
return err
}
return nil
}(); err != nil {
return errors.Wrap(err, "decode field \"error\"")
}
default:
return d.Skip()
}
return nil
}); err != nil {
return errors.Wrap(err, "decode AddPageBadRequest")
}
// Validate required fields.
var failures []validate.FieldError
for i, mask := range [1]uint8{
0b00000011,
} {
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(jsonFieldsNameOfAddPageBadRequest) {
name = jsonFieldsNameOfAddPageBadRequest[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 *AddPageBadRequest) MarshalJSON() ([]byte, error) {
e := jx.Encoder{}
s.Encode(&e)
return e.Bytes(), nil
}
// UnmarshalJSON implements stdjson.Unmarshaler.
func (s *AddPageBadRequest) UnmarshalJSON(data []byte) error {
d := jx.DecodeBytes(data)
return s.Decode(d)
}
// Encode implements json.Marshaler. // Encode implements json.Marshaler.
func (s *AddPageReq) Encode(e *jx.Encoder) { func (s *AddPageReq) Encode(e *jx.Encoder) {
e.ObjStart() e.ObjStart()

View File

@@ -15,7 +15,7 @@ import (
"github.com/ogen-go/ogen/validate" "github.com/ogen-go/ogen/validate"
) )
func decodeAddPageResponse(resp *http.Response) (res *Page, err error) { func decodeAddPageResponse(resp *http.Response) (res AddPageRes, err error) {
switch resp.StatusCode { switch resp.StatusCode {
case 201: case 201:
// Code 201. // Code 201.
@@ -52,6 +52,41 @@ func decodeAddPageResponse(resp *http.Response) (res *Page, err error) {
default: default:
return res, validate.InvalidContentType(ct) 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 AddPageBadRequest
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)
}
} }
// Convenient error response. // Convenient error response.
defRes, err := func() (res *ErrorStatusCode, err error) { defRes, err := func() (res *ErrorStatusCode, err error) {

View File

@@ -12,7 +12,9 @@ import (
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
) )
func encodeAddPageResponse(response *Page, w http.ResponseWriter, span trace.Span) error { func encodeAddPageResponse(response AddPageRes, w http.ResponseWriter, span trace.Span) error {
switch response := response.(type) {
case *Page:
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(201) w.WriteHeader(201)
span.SetStatus(codes.Ok, http.StatusText(201)) span.SetStatus(codes.Ok, http.StatusText(201))
@@ -23,6 +25,22 @@ func encodeAddPageResponse(response *Page, w http.ResponseWriter, span trace.Spa
return errors.Wrap(err, "write") return errors.Wrap(err, "write")
} }
return nil return nil
case *AddPageBadRequest:
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(400)
span.SetStatus(codes.Error, http.StatusText(400))
e := jx.GetEncoder()
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 encodeGetFileResponse(response GetFileRes, w http.ResponseWriter, span trace.Span) error { func encodeGetFileResponse(response GetFileRes, w http.ResponseWriter, span trace.Span) error {

View File

@@ -15,6 +15,33 @@ func (s *ErrorStatusCode) Error() string {
return fmt.Sprintf("code %d: %+v", s.StatusCode, s.Response) return fmt.Sprintf("code %d: %+v", s.StatusCode, s.Response)
} }
type AddPageBadRequest struct {
Field string `json:"field"`
Error string `json:"error"`
}
// GetField returns the value of Field.
func (s *AddPageBadRequest) GetField() string {
return s.Field
}
// GetError returns the value of Error.
func (s *AddPageBadRequest) GetError() string {
return s.Error
}
// SetField sets the value of Field.
func (s *AddPageBadRequest) SetField(val string) {
s.Field = val
}
// SetError sets the value of Error.
func (s *AddPageBadRequest) SetError(val string) {
s.Error = val
}
func (*AddPageBadRequest) addPageRes() {}
type AddPageReq struct { type AddPageReq struct {
URL string `json:"url"` URL string `json:"url"`
Description OptString `json:"description"` Description OptString `json:"description"`
@@ -349,6 +376,8 @@ func (s *Page) SetStatus(val Status) {
s.Status = val s.Status = val
} }
func (*Page) addPageRes() {}
// Merged schema. // Merged schema.
// Ref: #/components/schemas/pageWithResults // Ref: #/components/schemas/pageWithResults
type PageWithResults struct { type PageWithResults struct {

View File

@@ -13,7 +13,7 @@ type Handler interface {
// Add new page. // Add new page.
// //
// POST /pages // POST /pages
AddPage(ctx context.Context, req OptAddPageReq, params AddPageParams) (*Page, error) AddPage(ctx context.Context, req OptAddPageReq, params AddPageParams) (AddPageRes, error)
// GetFile implements getFile operation. // GetFile implements getFile operation.
// //
// Get file content. // Get file content.

View File

@@ -18,7 +18,7 @@ var _ Handler = UnimplementedHandler{}
// Add new page. // Add new page.
// //
// POST /pages // POST /pages
func (UnimplementedHandler) AddPage(ctx context.Context, req OptAddPageReq, params AddPageParams) (r *Page, _ error) { func (UnimplementedHandler) AddPage(ctx context.Context, req OptAddPageReq, params AddPageParams) (r AddPageRes, _ error) {
return r, ht.ErrNotImplemented return r, ht.ErrNotImplemented
} }

View File

@@ -1,6 +1,8 @@
package rest package rest
import ( import (
"fmt"
"github.com/derfenix/webarchive/api/openapi" "github.com/derfenix/webarchive/api/openapi"
"github.com/derfenix/webarchive/entity" "github.com/derfenix/webarchive/entity"
) )
@@ -93,7 +95,7 @@ func StatusToRest(s entity.Status) openapi.Status {
} }
} }
func FormatFromRest(format []openapi.Format) []entity.Format { func FormatFromRest(format []openapi.Format) ([]entity.Format, error) {
var formats []entity.Format var formats []entity.Format
switch { switch {
@@ -112,11 +114,14 @@ func FormatFromRest(format []openapi.Format) []entity.Format {
case openapi.FormatSingleFile: case openapi.FormatSingleFile:
formats[i] = entity.FormatSingleFile formats[i] = entity.FormatSingleFile
default:
return nil, fmt.Errorf("invalid format value %s", format)
} }
} }
} }
return formats return formats, nil
} }
func FormatToRest(format entity.Format) openapi.Format { func FormatToRest(format entity.Format) openapi.Format {

View File

@@ -41,7 +41,7 @@ func (s *Service) GetPage(ctx context.Context, params openapi.GetPageParams) (op
return &restPage, nil return &restPage, nil
} }
func (s *Service) AddPage(ctx context.Context, req openapi.OptAddPageReq, params openapi.AddPageParams) (*openapi.Page, error) { func (s *Service) AddPage(ctx context.Context, req openapi.OptAddPageReq, params openapi.AddPageParams) (openapi.AddPageRes, error) {
url := params.URL.Or(req.Value.URL) url := params.URL.Or(req.Value.URL)
description := params.Description.Or(req.Value.Description.Value) description := params.Description.Or(req.Value.Description.Value)
@@ -60,19 +60,32 @@ func (s *Service) AddPage(ctx context.Context, req openapi.OptAddPageReq, params
url = params.URL.Value url = params.URL.Value
} }
page := entity.NewPage(url, description, FormatFromRest(formats)...) if url == "" {
return &openapi.AddPageBadRequest{
Field: "url",
Error: "Value is required",
}, nil
}
domainFormats, err := FormatFromRest(formats)
if err != nil {
return &openapi.AddPageBadRequest{
Field: "formats",
Error: err.Error(),
}, nil
}
page := entity.NewPage(url, description, domainFormats...)
page.Status = entity.StatusProcessing page.Status = entity.StatusProcessing
err := s.pages.Save(ctx, page) if err := s.pages.Save(ctx, page); err != nil {
if err != nil {
return nil, fmt.Errorf("save page: %w", err) return nil, fmt.Errorf("save page: %w", err)
} }
s.ch <- page
res := PageToRest(page) res := PageToRest(page)
s.ch <- page
return &res, nil return &res, nil
} }