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:
schema:
$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:
$ref: '#/components/responses/undefinedError'
@@ -109,7 +125,7 @@ paths:
200:
description: File content
content:
application/pdf: {}
application/pdf: { }
text/plain:
schema:
type: string

View File

@@ -76,13 +76,13 @@ func (c *Client) requestURL(ctx context.Context) *url.URL {
// Add new page.
//
// 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
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{
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 {
mreq := middleware.Request{
Context: ctx,
@@ -113,7 +113,7 @@ func (s *Server) handleAddPageRequest(args [0]string, argsEscaped bool, w http.R
type (
Request = OptAddPageReq
Params = AddPageParams
Response = *Page
Response = AddPageRes
)
response, err = middleware.HookMiddleware[
Request,

View File

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

View File

@@ -13,6 +13,121 @@ import (
"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.
func (s *AddPageReq) Encode(e *jx.Encoder) {
e.ObjStart()

View File

@@ -15,7 +15,7 @@ import (
"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 {
case 201:
// Code 201.
@@ -52,6 +52,41 @@ func decodeAddPageResponse(resp *http.Response) (res *Page, err error) {
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 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.
defRes, err := func() (res *ErrorStatusCode, err error) {

View File

@@ -12,7 +12,9 @@ import (
"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.WriteHeader(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 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 {

View File

@@ -15,6 +15,33 @@ func (s *ErrorStatusCode) Error() string {
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 {
URL string `json:"url"`
Description OptString `json:"description"`
@@ -349,6 +376,8 @@ func (s *Page) SetStatus(val Status) {
s.Status = val
}
func (*Page) addPageRes() {}
// Merged schema.
// Ref: #/components/schemas/pageWithResults
type PageWithResults struct {

View File

@@ -13,7 +13,7 @@ type Handler interface {
// Add new page.
//
// 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.
//
// Get file content.

View File

@@ -18,7 +18,7 @@ var _ Handler = UnimplementedHandler{}
// Add new page.
//
// 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
}

View File

@@ -1,6 +1,8 @@
package rest
import (
"fmt"
"github.com/derfenix/webarchive/api/openapi"
"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
switch {
@@ -112,11 +114,14 @@ func FormatFromRest(format []openapi.Format) []entity.Format {
case openapi.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 {

View File

@@ -41,7 +41,7 @@ func (s *Service) GetPage(ctx context.Context, params openapi.GetPageParams) (op
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)
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
}
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
err := s.pages.Save(ctx, page)
if err != nil {
if err := s.pages.Save(ctx, page); err != nil {
return nil, fmt.Errorf("save page: %w", err)
}
s.ch <- page
res := PageToRest(page)
s.ch <- page
return &res, nil
}