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
|
# Content-addressed media files
|
||||||
/data
|
/data
|
||||||
|
/data0
|
||||||
|
|
||||||
# Test binary, built with `go test -c`
|
# Test binary, built with `go test -c`
|
||||||
*.test
|
*.test
|
||||||
|
|||||||
12
album.go
12
album.go
@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -108,13 +109,16 @@ func (a *Album) Enrich(ctx context.Context, n Net) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var v AlbumResponse
|
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
|
return err
|
||||||
|
} else if resp.StatusCode != http.StatusOK {
|
||||||
|
return &ResponseStatusError{u, resp.StatusCode, resp.Body.Close()}
|
||||||
} else {
|
} else {
|
||||||
if err = json.NewDecoder(body).Decode(&v); err != nil {
|
if err = json.NewDecoder(resp.Body).Decode(&v); err != nil {
|
||||||
return errors.Join(body.Close(), err)
|
return errors.Join(resp.Body.Close(), err)
|
||||||
}
|
}
|
||||||
if err = body.Close(); err != nil {
|
if err = resp.Body.Close(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -183,6 +183,19 @@ func TestAlbumEnrich(t *testing.T) {
|
|||||||
makeAlbumPath(1030, true): []byte{0},
|
makeAlbumPath(1030, true): []byte{0},
|
||||||
}, Album{}, errors.Join(newSyntaxError("invalid character '\\x00' looking for beginning of value", 1))},
|
}, 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{
|
{"close", Album{
|
||||||
CID: 1030,
|
CID: 1030,
|
||||||
Name: "Speed of Light",
|
Name: "Speed of Light",
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"slices"
|
"slices"
|
||||||
@ -97,19 +98,21 @@ func mustFetch(ctx context.Context) {
|
|||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for u := range uc {
|
for u := range uc {
|
||||||
buf := new(bytes.Buffer)
|
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)
|
log.Fatal(err)
|
||||||
|
} else if resp.StatusCode != http.StatusOK {
|
||||||
|
log.Fatal(&monstersirenfetch.ResponseStatusError{URL: u, StatusCode: resp.StatusCode, CloseErr: resp.Body.Close()})
|
||||||
} else {
|
} else {
|
||||||
if v := int(l); v > 0 {
|
if v := int(resp.ContentLength); v > 0 {
|
||||||
buf.Grow(v)
|
buf.Grow(v)
|
||||||
}
|
}
|
||||||
if _, err = io.Copy(buf, r); err != nil {
|
if _, err = io.Copy(buf, resp.Body); err != nil {
|
||||||
if closeErr := r.Close(); closeErr != nil {
|
if closeErr := resp.Body.Close(); closeErr != nil {
|
||||||
log.Print(closeErr)
|
log.Print(closeErr)
|
||||||
}
|
}
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
if err = r.Close(); err != nil {
|
if err = resp.Body.Close(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -10,12 +9,12 @@ type netDirect struct {
|
|||||||
c http.Client
|
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
|
var resp *http.Response
|
||||||
if req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil); err != nil {
|
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 {
|
} 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"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@ -30,7 +31,21 @@ func (e *InconsistentEnrichError[T]) Error() string {
|
|||||||
type Net interface {
|
type Net interface {
|
||||||
// Get makes a get request to url and returns the response body.
|
// Get makes a get request to url and returns the response body.
|
||||||
// The caller must close the body after it finishes reading from it.
|
// 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.
|
// Metadata represents metadata, often enriched, of the entire website.
|
||||||
|
|||||||
@ -6,9 +6,11 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"unsafe"
|
"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) {
|
func TestNullableString(t *testing.T) {
|
||||||
checkJSONRoundTripM(t, []jsonRoundTripTestCase[NullableString]{
|
checkJSONRoundTripM(t, []jsonRoundTripTestCase[NullableString]{
|
||||||
{"zero", nil, `null`, ""},
|
{"zero", nil, `null`, ""},
|
||||||
@ -116,21 +125,25 @@ func checkJSONRoundTrip[T any](t *testing.T, v T, data []byte) {
|
|||||||
|
|
||||||
type stubNetErrorCloser stubNet
|
type stubNetErrorCloser stubNet
|
||||||
|
|
||||||
func (n stubNetErrorCloser) Get(ctx context.Context, url string) (io.ReadCloser, int64, error) {
|
func (n stubNetErrorCloser) Get(ctx context.Context, url string) (*http.Response, error) {
|
||||||
r, l, err := stubNet(n).Get(ctx, url)
|
resp, err := stubNet(n).Get(ctx, url)
|
||||||
if r != nil {
|
if resp != nil && resp.Body != nil {
|
||||||
r = errorCloser{r}
|
resp.Body = errorCloser{resp.Body}
|
||||||
}
|
}
|
||||||
return r, l, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
type stubNet map[string][]byte
|
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 {
|
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 {
|
} 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"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
)
|
)
|
||||||
@ -79,13 +80,16 @@ func (s *Song) Enrich(ctx context.Context, n Net) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var v SongResponse
|
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
|
return err
|
||||||
|
} else if resp.StatusCode != http.StatusOK {
|
||||||
|
return &ResponseStatusError{u, resp.StatusCode, resp.Body.Close()}
|
||||||
} else {
|
} else {
|
||||||
if err = json.NewDecoder(body).Decode(&v); err != nil {
|
if err = json.NewDecoder(resp.Body).Decode(&v); err != nil {
|
||||||
return errors.Join(body.Close(), err)
|
return errors.Join(resp.Body.Close(), err)
|
||||||
}
|
}
|
||||||
if err = body.Close(); err != nil {
|
if err = resp.Body.Close(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
song_test.go
13
song_test.go
@ -113,6 +113,19 @@ func TestSongEnrich(t *testing.T) {
|
|||||||
makeSongPath(48794): []byte{0},
|
makeSongPath(48794): []byte{0},
|
||||||
}, Song{}, errors.Join(newSyntaxError("invalid character '\\x00' looking for beginning of value", 1))},
|
}, 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{
|
{"close", Song{
|
||||||
CID: 48794,
|
CID: 48794,
|
||||||
Name: "Warm and Small Light",
|
Name: "Warm and Small Light",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user