generic: net return full response struct
This is still quite easy to stub, and the extra information is very useful to the caller. Signed-off-by: Yonah <contrib@gensokyo.uk>
This commit is contained in:
parent
5fe911dd01
commit
f48506b1ca
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,6 +10,7 @@
|
||||
|
||||
# Content-addressed media files
|
||||
/data
|
||||
/data0
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
12
album.go
12
album.go
@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
@ -108,13 +109,16 @@ func (a *Album) Enrich(ctx context.Context, n Net) error {
|
||||
}
|
||||
|
||||
var v AlbumResponse
|
||||
if body, _, err := n.Get(ctx, APIPrefix+"/album/"+a.CID.String()+"/detail"); err != nil {
|
||||
u := APIPrefix + "/album/" + a.CID.String() + "/detail"
|
||||
if resp, err := n.Get(ctx, u); err != nil {
|
||||
return err
|
||||
} else if resp.StatusCode != http.StatusOK {
|
||||
return &ResponseStatusError{u, resp.StatusCode, resp.Body.Close()}
|
||||
} else {
|
||||
if err = json.NewDecoder(body).Decode(&v); err != nil {
|
||||
return errors.Join(body.Close(), err)
|
||||
if err = json.NewDecoder(resp.Body).Decode(&v); err != nil {
|
||||
return errors.Join(resp.Body.Close(), err)
|
||||
}
|
||||
if err = body.Close(); err != nil {
|
||||
if err = resp.Body.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@ -183,6 +183,19 @@ func TestAlbumEnrich(t *testing.T) {
|
||||
makeAlbumPath(1030, true): []byte{0},
|
||||
}, Album{}, errors.Join(newSyntaxError("invalid character '\\x00' looking for beginning of value", 1))},
|
||||
|
||||
{"status", Album{
|
||||
CID: 1030,
|
||||
Name: "Speed of Light",
|
||||
CoverURL: "https://web.hycdn.cn/siren/pic/20210322/56cbcd1d0093d8ee8ee22bf6d68ab4a6.jpg",
|
||||
Artists: []string{"塞壬唱片-MSR", "DJ Okawari"},
|
||||
}, stubNetErrorCloser{
|
||||
makeAlbumPath(1030, true): []byte{0xde, 0xad, 0xbe, 0xef},
|
||||
}, Album{}, &ResponseStatusError{
|
||||
URL: "https://monster-siren.hypergryph.com/api/album/1030/detail",
|
||||
StatusCode: 0xdeadbeef,
|
||||
CloseErr: os.ErrInvalid,
|
||||
}},
|
||||
|
||||
{"close", Album{
|
||||
CID: 1030,
|
||||
Name: "Speed of Light",
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"flag"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
@ -97,19 +98,21 @@ func mustFetch(ctx context.Context) {
|
||||
defer wg.Done()
|
||||
for u := range uc {
|
||||
buf := new(bytes.Buffer)
|
||||
if r, l, err := n.Get(ctx, u); err != nil {
|
||||
if resp, err := n.Get(ctx, u); err != nil {
|
||||
log.Fatal(err)
|
||||
} else if resp.StatusCode != http.StatusOK {
|
||||
log.Fatal(&monstersirenfetch.ResponseStatusError{URL: u, StatusCode: resp.StatusCode, CloseErr: resp.Body.Close()})
|
||||
} else {
|
||||
if v := int(l); v > 0 {
|
||||
if v := int(resp.ContentLength); v > 0 {
|
||||
buf.Grow(v)
|
||||
}
|
||||
if _, err = io.Copy(buf, r); err != nil {
|
||||
if closeErr := r.Close(); closeErr != nil {
|
||||
if _, err = io.Copy(buf, resp.Body); err != nil {
|
||||
if closeErr := resp.Body.Close(); closeErr != nil {
|
||||
log.Print(closeErr)
|
||||
}
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err = r.Close(); err != nil {
|
||||
if err = resp.Body.Close(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@ -10,12 +9,12 @@ type netDirect struct {
|
||||
c http.Client
|
||||
}
|
||||
|
||||
func (n *netDirect) Get(ctx context.Context, url string) (io.ReadCloser, int64, error) {
|
||||
func (n *netDirect) Get(ctx context.Context, url string) (*http.Response, error) {
|
||||
var resp *http.Response
|
||||
if req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil); err != nil {
|
||||
return nil, -2, err
|
||||
return nil, err
|
||||
} else if resp, err = n.c.Do(req); err != nil {
|
||||
return nil, -2, err
|
||||
return nil, err
|
||||
}
|
||||
return resp.Body, resp.ContentLength, nil
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
17
generic.go
17
generic.go
@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@ -30,7 +31,21 @@ func (e *InconsistentEnrichError[T]) Error() string {
|
||||
type Net interface {
|
||||
// Get makes a get request to url and returns the response body.
|
||||
// The caller must close the body after it finishes reading from it.
|
||||
Get(ctx context.Context, url string) (body io.ReadCloser, contentLength int64, err error)
|
||||
Get(ctx context.Context, url string) (resp *http.Response, err error)
|
||||
}
|
||||
|
||||
// ResponseStatusError is returned when MSR responds with a StatusCode other than [http.StatusOK].
|
||||
type ResponseStatusError struct {
|
||||
// URL is the full uninterpreted url string for this request.
|
||||
URL string
|
||||
// StatusCode holds the [http.Response] field of the same name.
|
||||
StatusCode int
|
||||
// CloseErr holds the error returned closing the response body.
|
||||
CloseErr error
|
||||
}
|
||||
|
||||
func (e *ResponseStatusError) Error() string {
|
||||
return e.URL + " responded with status code " + strconv.Itoa(e.StatusCode)
|
||||
}
|
||||
|
||||
// Metadata represents metadata, often enriched, of the entire website.
|
||||
|
||||
@ -6,9 +6,11 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
@ -22,6 +24,13 @@ func TestInconsistentEnrichError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseStatusError(t *testing.T) {
|
||||
const want = "/proc/nonexistent responded with status code 404"
|
||||
if got := (&ResponseStatusError{URL: "/proc/nonexistent", StatusCode: http.StatusNotFound, CloseErr: os.ErrInvalid}).Error(); got != want {
|
||||
t.Errorf("Error: %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNullableString(t *testing.T) {
|
||||
checkJSONRoundTripM(t, []jsonRoundTripTestCase[NullableString]{
|
||||
{"zero", nil, `null`, ""},
|
||||
@ -116,21 +125,25 @@ func checkJSONRoundTrip[T any](t *testing.T, v T, data []byte) {
|
||||
|
||||
type stubNetErrorCloser stubNet
|
||||
|
||||
func (n stubNetErrorCloser) Get(ctx context.Context, url string) (io.ReadCloser, int64, error) {
|
||||
r, l, err := stubNet(n).Get(ctx, url)
|
||||
if r != nil {
|
||||
r = errorCloser{r}
|
||||
func (n stubNetErrorCloser) Get(ctx context.Context, url string) (*http.Response, error) {
|
||||
resp, err := stubNet(n).Get(ctx, url)
|
||||
if resp != nil && resp.Body != nil {
|
||||
resp.Body = errorCloser{resp.Body}
|
||||
}
|
||||
return r, l, err
|
||||
return resp, err
|
||||
}
|
||||
|
||||
type stubNet map[string][]byte
|
||||
|
||||
func (n stubNet) Get(_ context.Context, url string) (io.ReadCloser, int64, error) {
|
||||
func (n stubNet) Get(_ context.Context, url string) (*http.Response, error) {
|
||||
if b, ok := n[url]; !ok {
|
||||
return nil, -2, fmt.Errorf("url %s requested, but is not present", url)
|
||||
return nil, fmt.Errorf("url %s requested, but is not present", url)
|
||||
} else if string(b) == "\xde\xad\xbe\xef" {
|
||||
return &http.Response{StatusCode: 0xdeadbeef, ContentLength: 0xdeadbeef,
|
||||
Body: errorCloser{nopCloser{strings.NewReader("\xde\xad\xbe\xef")}}}, nil
|
||||
} else {
|
||||
return nopCloser{bytes.NewReader(b)}, int64(len(b)), nil
|
||||
return &http.Response{StatusCode: http.StatusOK, ContentLength: int64(len(b)),
|
||||
Body: nopCloser{bytes.NewReader(b)}}, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
12
song.go
12
song.go
@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
)
|
||||
@ -79,13 +80,16 @@ func (s *Song) Enrich(ctx context.Context, n Net) error {
|
||||
}
|
||||
|
||||
var v SongResponse
|
||||
if body, _, err := n.Get(ctx, APIPrefix+"/song/"+s.CID.String()); err != nil {
|
||||
u := APIPrefix + "/song/" + s.CID.String()
|
||||
if resp, err := n.Get(ctx, u); err != nil {
|
||||
return err
|
||||
} else if resp.StatusCode != http.StatusOK {
|
||||
return &ResponseStatusError{u, resp.StatusCode, resp.Body.Close()}
|
||||
} else {
|
||||
if err = json.NewDecoder(body).Decode(&v); err != nil {
|
||||
return errors.Join(body.Close(), err)
|
||||
if err = json.NewDecoder(resp.Body).Decode(&v); err != nil {
|
||||
return errors.Join(resp.Body.Close(), err)
|
||||
}
|
||||
if err = body.Close(); err != nil {
|
||||
if err = resp.Body.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
13
song_test.go
13
song_test.go
@ -113,6 +113,19 @@ func TestSongEnrich(t *testing.T) {
|
||||
makeSongPath(48794): []byte{0},
|
||||
}, Song{}, errors.Join(newSyntaxError("invalid character '\\x00' looking for beginning of value", 1))},
|
||||
|
||||
{"status", Song{
|
||||
CID: 48794,
|
||||
Name: "Warm and Small Light",
|
||||
AlbumCID: 6660,
|
||||
Artists: []string{"塞壬唱片-MSR"},
|
||||
}, stubNet{
|
||||
makeSongPath(48794): []byte{0xde, 0xad, 0xbe, 0xef},
|
||||
}, Song{}, &ResponseStatusError{
|
||||
URL: "https://monster-siren.hypergryph.com/api/song/048794",
|
||||
StatusCode: 0xdeadbeef,
|
||||
CloseErr: os.ErrInvalid,
|
||||
}},
|
||||
|
||||
{"close", Song{
|
||||
CID: 48794,
|
||||
Name: "Warm and Small Light",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user