From 11a6171954e92f6428511c540a27d947ae10b11a Mon Sep 17 00:00:00 2001 From: derfenix Date: Sun, 2 Apr 2023 17:31:37 +0300 Subject: [PATCH] Improve API --- api/openapi.yaml | 18 +++- api/openapi/oas_client_gen.go | 4 +- api/openapi/oas_handlers_gen.go | 4 +- api/openapi/oas_interfaces_gen.go | 4 + api/openapi/oas_json_gen.go | 115 +++++++++++++++++++++++ api/openapi/oas_response_decoders_gen.go | 37 +++++++- api/openapi/oas_response_encoders_gen.go | 36 +++++-- api/openapi/oas_schemas_gen.go | 29 ++++++ api/openapi/oas_server_gen.go | 2 +- api/openapi/oas_unimplemented_gen.go | 2 +- ports/rest/converter.go | 9 +- ports/rest/service.go | 25 +++-- 12 files changed, 260 insertions(+), 25 deletions(-) diff --git a/api/openapi.yaml b/api/openapi.yaml index 2ccac4a..e5719c2 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -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 diff --git a/api/openapi/oas_client_gen.go b/api/openapi/oas_client_gen.go index 36eed1a..d34010c 100644 --- a/api/openapi/oas_client_gen.go +++ b/api/openapi/oas_client_gen.go @@ -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"), } diff --git a/api/openapi/oas_handlers_gen.go b/api/openapi/oas_handlers_gen.go index 6fe2fd0..a5fd482 100644 --- a/api/openapi/oas_handlers_gen.go +++ b/api/openapi/oas_handlers_gen.go @@ -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, diff --git a/api/openapi/oas_interfaces_gen.go b/api/openapi/oas_interfaces_gen.go index b1b2f70..5c76d59 100644 --- a/api/openapi/oas_interfaces_gen.go +++ b/api/openapi/oas_interfaces_gen.go @@ -1,6 +1,10 @@ // Code generated by ogen, DO NOT EDIT. package openapi +type AddPageRes interface { + addPageRes() +} + type GetFileRes interface { getFileRes() } diff --git a/api/openapi/oas_json_gen.go b/api/openapi/oas_json_gen.go index 4e63dbf..5f5e1e0 100644 --- a/api/openapi/oas_json_gen.go +++ b/api/openapi/oas_json_gen.go @@ -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() diff --git a/api/openapi/oas_response_decoders_gen.go b/api/openapi/oas_response_decoders_gen.go index 96f4f71..77171cd 100644 --- a/api/openapi/oas_response_decoders_gen.go +++ b/api/openapi/oas_response_decoders_gen.go @@ -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) { diff --git a/api/openapi/oas_response_encoders_gen.go b/api/openapi/oas_response_encoders_gen.go index b0ecbaa..3285b30 100644 --- a/api/openapi/oas_response_encoders_gen.go +++ b/api/openapi/oas_response_encoders_gen.go @@ -12,17 +12,35 @@ import ( "go.opentelemetry.io/otel/trace" ) -func encodeAddPageResponse(response *Page, w http.ResponseWriter, span trace.Span) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(201) - span.SetStatus(codes.Ok, http.StatusText(201)) +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)) - e := jx.GetEncoder() - response.Encode(e) - if _, err := e.WriteTo(w); err != nil { - return errors.Wrap(err, "write") + e := jx.GetEncoder() + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + 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) } - return nil } func encodeGetFileResponse(response GetFileRes, w http.ResponseWriter, span trace.Span) error { diff --git a/api/openapi/oas_schemas_gen.go b/api/openapi/oas_schemas_gen.go index b581f94..dda0d7e 100644 --- a/api/openapi/oas_schemas_gen.go +++ b/api/openapi/oas_schemas_gen.go @@ -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 { diff --git a/api/openapi/oas_server_gen.go b/api/openapi/oas_server_gen.go index 05a094a..89d5405 100644 --- a/api/openapi/oas_server_gen.go +++ b/api/openapi/oas_server_gen.go @@ -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. diff --git a/api/openapi/oas_unimplemented_gen.go b/api/openapi/oas_unimplemented_gen.go index d2c2849..ce775b2 100644 --- a/api/openapi/oas_unimplemented_gen.go +++ b/api/openapi/oas_unimplemented_gen.go @@ -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 } diff --git a/ports/rest/converter.go b/ports/rest/converter.go index 8459092..efa3ff6 100644 --- a/ports/rest/converter.go +++ b/ports/rest/converter.go @@ -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 { diff --git a/ports/rest/service.go b/ports/rest/service.go index a70654e..bb9ad16 100644 --- a/ports/rest/service.go +++ b/ports/rest/service.go @@ -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 }