🎉 Initial commit
This commit is contained in:
commit
6e486be3af
53 changed files with 6462 additions and 0 deletions
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
*.jpg filter=lfs diff=lfs merge=lfs -text
|
2
README.md
Normal file
2
README.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
# MicroPub
|
||||
> Personal micropub server
|
8
go.mod
Normal file
8
go.mod
Normal file
|
@ -0,0 +1,8 @@
|
|||
module source.toby3d.me/toby3d/pub
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/google/go-cmp v0.5.9
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2
|
||||
)
|
4
go.sum
Normal file
4
go.sum
Normal file
|
@ -0,0 +1,4 @@
|
|||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
23
internal/common/common.go
Normal file
23
internal/common/common.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package common
|
||||
|
||||
const charsetUTF8 = "charset=UTF-8"
|
||||
|
||||
const (
|
||||
HeaderAcceptLanguage string = "Accept-Language"
|
||||
HeaderContentType string = "Content-Type"
|
||||
HeaderLocation string = "Location"
|
||||
HeaderXContentTypeOptions string = "X-Content-Type-Options"
|
||||
)
|
||||
|
||||
const (
|
||||
MIMEApplicationForm string = "application/x-www-form-urlencoded"
|
||||
MIMEApplicationFormCharsetUTF8 string = MIMEApplicationForm + "; " + charsetUTF8
|
||||
MIMEApplicationJSON string = "application/json"
|
||||
MIMEApplicationJSONCharsetUTF8 string = MIMEApplicationJSON + "; " + charsetUTF8
|
||||
MIMEMultipartForm string = "multipart/form-data"
|
||||
MIMEMultipartFormCharsetUTF8 string = MIMEMultipartForm + "; " + charsetUTF8
|
||||
MIMETextHTML string = "text/html"
|
||||
MIMETextHTMLCharsetUTF8 string = MIMETextHTML + "; " + charsetUTF8
|
||||
MIMETextPlain string = "text/plain"
|
||||
MIMETextPlainCharsetUTF8 string = MIMETextPlain + "; " + charsetUTF8
|
||||
)
|
45
internal/domain/config.go
Normal file
45
internal/domain/config.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type (
|
||||
// Config represent a global micropub instance configuration.
|
||||
Config struct {
|
||||
HTTP ConfigHTTP `envPrefix:"HTTP_"`
|
||||
MediaDir string `env:"MEDIA_DIR" envDefault:"media"`
|
||||
}
|
||||
|
||||
// ConfigHTTP represents HTTP configs which used for instance serving
|
||||
// and Location header responses.
|
||||
ConfigHTTP struct {
|
||||
Bind string `env:"BIND" envDefault:":3000"`
|
||||
Host string `env:"HOST" envDefault:"localhost:3000"`
|
||||
Proto string `env:"PROTO" envDefault:"http"`
|
||||
}
|
||||
)
|
||||
|
||||
// TestConfig returns a valid Config for tests.
|
||||
func TestConfig(tb testing.TB) *Config {
|
||||
tb.Helper()
|
||||
|
||||
return &Config{
|
||||
HTTP: ConfigHTTP{
|
||||
Bind: ":3000",
|
||||
Host: "example.com",
|
||||
Proto: "https",
|
||||
},
|
||||
MediaDir: "media",
|
||||
}
|
||||
}
|
||||
|
||||
// BaseURL returns root *url.URL based on provided proto and host.
|
||||
func (c ConfigHTTP) BaseURL() *url.URL {
|
||||
return &url.URL{
|
||||
Scheme: c.Proto,
|
||||
Host: c.Host,
|
||||
Path: "/",
|
||||
}
|
||||
}
|
36
internal/domain/error.go
Normal file
36
internal/domain/error.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// Error represent a custom error implementation with HTTP status codes support.
|
||||
//
|
||||
//nolint:tagliatelle
|
||||
type Error struct {
|
||||
Description string `json:"error_description,omitempty"`
|
||||
Frame xerrors.Frame `json:"-"`
|
||||
Code int `json:"error"`
|
||||
}
|
||||
|
||||
func (err Error) Error() string {
|
||||
return fmt.Sprint(err)
|
||||
}
|
||||
|
||||
func (err Error) Format(f fmt.State, r rune) {
|
||||
xerrors.FormatError(err, f, r)
|
||||
}
|
||||
|
||||
func (err Error) FormatError(p xerrors.Printer) error {
|
||||
p.Printf("%d: %s", err.Code, err.Description)
|
||||
|
||||
if !p.Detail() {
|
||||
return err
|
||||
}
|
||||
|
||||
err.Frame.Format(p)
|
||||
|
||||
return nil
|
||||
}
|
72
internal/domain/file.go
Normal file
72
internal/domain/file.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io"
|
||||
"mime"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// File represent a single media file, like photo.
|
||||
type File struct {
|
||||
Path string // content/example/photo.jpg
|
||||
Content []byte
|
||||
}
|
||||
|
||||
//go:embed testdata/sunset.jpg
|
||||
var testdata embed.FS
|
||||
|
||||
// TestFile returns a valid File for tests.
|
||||
func TestFile(tb testing.TB) *File {
|
||||
tb.Helper()
|
||||
|
||||
f, err := testdata.Open(filepath.Join("testdata", "sunset.jpg"))
|
||||
if err != nil {
|
||||
tb.Fatalf("cannot open testing file: %s", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
tb.Fatalf("cannot fetch testing file info: %s", err)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
tb.Fatalf("cannot read testing file body: %s", err)
|
||||
}
|
||||
|
||||
return &File{
|
||||
Path: info.Name(),
|
||||
Content: body,
|
||||
}
|
||||
}
|
||||
|
||||
// LogicalName returns full file name without directory path.
|
||||
func (f File) LogicalName() string {
|
||||
return filepath.Base(f.Path)
|
||||
}
|
||||
|
||||
// BaseFileName returns file name without extention and directory path.
|
||||
func (f File) BaseFileName() string {
|
||||
base := filepath.Base(f.Path)
|
||||
|
||||
return strings.TrimSuffix(base, filepath.Ext(base))
|
||||
}
|
||||
|
||||
// Ext returns file extention.
|
||||
func (f File) Ext() string {
|
||||
return filepath.Ext(f.Path)
|
||||
}
|
||||
|
||||
// Dir returns file directory.
|
||||
func (f File) Dir() string {
|
||||
return filepath.Dir(f.Path)
|
||||
}
|
||||
|
||||
// MediaType returns media type based on file extention.
|
||||
func (f File) MediaType() string {
|
||||
return mime.TypeByExtension(f.Ext())
|
||||
}
|
BIN
internal/domain/testdata/sunset.jpg
(Stored with Git LFS)
vendored
Normal file
BIN
internal/domain/testdata/sunset.jpg
(Stored with Git LFS)
vendored
Normal file
Binary file not shown.
18
internal/media/delivery/http/doc.go
Normal file
18
internal/media/delivery/http/doc.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
// Package provides a media HTTP endpoints.
|
||||
//
|
||||
// To upload a file to the Media Endpoint, the client sends a
|
||||
// `multipart/form-data` request with one part named file. The Media Endpoint
|
||||
// MAY ignore the suggested filename that the client sends.
|
||||
//
|
||||
// The Media Endpoint processes the file upload, storing it in whatever backend
|
||||
// it wishes, and generates a URL to the file. The URL SHOULD be unguessable,
|
||||
// such as using a UUID in the path. If the request is successful, the endpoint
|
||||
// MUST return the URL to the file that was created in the HTTP Location header,
|
||||
// and respond with HTTP 201 Created. The response body is left undefined.
|
||||
//
|
||||
// The Micropub client can then use this URL as the value of e.g. the "photo"
|
||||
// property of a Micropub request.
|
||||
//
|
||||
// The Media Endpoint MAY periodically delete files uploaded if they are not
|
||||
// used in a Micropub request within a specific amount of time.
|
||||
package http
|
99
internal/media/delivery/http/media_http.go
Normal file
99
internal/media/delivery/http/media_http.go
Normal file
|
@ -0,0 +1,99 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
|
||||
"source.toby3d.me/toby3d/pub/internal/common"
|
||||
"source.toby3d.me/toby3d/pub/internal/domain"
|
||||
"source.toby3d.me/toby3d/pub/internal/media"
|
||||
)
|
||||
|
||||
type (
|
||||
Handler struct {
|
||||
media media.UseCase
|
||||
config domain.Config
|
||||
}
|
||||
|
||||
Error struct {
|
||||
Error string `json:"error"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewHandler(media media.UseCase, config domain.Config) *Handler {
|
||||
return &Handler{
|
||||
media: media,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
WriteError(w, "method MUST be "+http.MethodPost, http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
mediaType, _, err := mime.ParseMediaType(r.Header.Get(common.HeaderContentType))
|
||||
if err != nil {
|
||||
WriteError(w, "Content-Type header MUST be "+common.MIMEMultipartForm, http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if mediaType != common.MIMEMultipartForm {
|
||||
WriteError(w, "Content-Type header MUST be "+common.MIMEMultipartForm, http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
file, head, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
WriteError(w, err.Error(), http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
in := &domain.File{
|
||||
Path: head.Filename,
|
||||
Content: make([]byte, 0),
|
||||
}
|
||||
|
||||
if in.Content, err = io.ReadAll(file); err != nil {
|
||||
WriteError(w, err.Error(), http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
out, err := h.media.Upload(r.Context(), *in)
|
||||
if err != nil {
|
||||
WriteError(w, err.Error(), http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set(common.HeaderLocation, h.config.HTTP.BaseURL().JoinPath(out.Path).String())
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
|
||||
func WriteError(w http.ResponseWriter, description string, status int) {
|
||||
out := &Error{ErrorDescription: description}
|
||||
|
||||
switch status {
|
||||
case http.StatusBadRequest:
|
||||
out.Error = "invalid_request"
|
||||
case http.StatusForbidden: // TODO(toby3d): insufficient_scope
|
||||
out.Error = "forbidden"
|
||||
case http.StatusUnauthorized:
|
||||
out.Error = "unauthorized"
|
||||
}
|
||||
|
||||
_ = json.NewEncoder(w).Encode(out)
|
||||
|
||||
w.Header().Set(common.HeaderContentType, common.MIMEApplicationJSONCharsetUTF8)
|
||||
w.WriteHeader(status)
|
||||
}
|
56
internal/media/delivery/http/media_http_test.go
Normal file
56
internal/media/delivery/http/media_http_test.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package http_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"source.toby3d.me/toby3d/pub/internal/common"
|
||||
"source.toby3d.me/toby3d/pub/internal/domain"
|
||||
"source.toby3d.me/toby3d/pub/internal/media"
|
||||
delivery "source.toby3d.me/toby3d/pub/internal/media/delivery/http"
|
||||
)
|
||||
|
||||
func TestUpload(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testConfig := domain.TestConfig(t)
|
||||
testFile := domain.TestFile(t)
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
form := multipart.NewWriter(buf)
|
||||
formWriter, err := form.CreateFormFile("file", "photo.jpg")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err = formWriter.Write(testFile.Content); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err = form.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expect := testConfig.HTTP.BaseURL().JoinPath("media", "abc123"+testFile.Ext())
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "https://example.com/media", buf)
|
||||
req.Header.Set(common.HeaderContentType, form.FormDataContentType())
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
delivery.NewHandler(
|
||||
media.NewStubUseCase(expect, testFile, nil), *testConfig).
|
||||
ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
t.Errorf("%s %s = %d, want %d", req.Method, req.RequestURI, resp.StatusCode, http.StatusCreated)
|
||||
}
|
||||
|
||||
if location := resp.Header.Get(common.HeaderLocation); location != expect.String() {
|
||||
t.Errorf("%s %s = %s, want not empty", req.Method, req.RequestURI, location)
|
||||
}
|
||||
}
|
10
internal/media/doc.go
Normal file
10
internal/media/doc.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
// Package media provide a better user experience for Micropub applications, as
|
||||
// well as to overcome the limitation of being unable to upload a file with the
|
||||
// JSON syntax, a Micropub server MAY support a "Media Endpoint". The role of the
|
||||
// Media Endpoint is exclusively to handle file uploads and return a URL that
|
||||
// can be used in a subsequent Micropub request.
|
||||
//
|
||||
// When a Micropub server supports a Media Endpoint, clients can start uploading
|
||||
// a photo or other media right away while the user is still creating other
|
||||
// parts of the post.
|
||||
package media
|
132
internal/media/repository.go
Normal file
132
internal/media/repository.go
Normal file
|
@ -0,0 +1,132 @@
|
|||
package media
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"source.toby3d.me/toby3d/pub/internal/domain"
|
||||
)
|
||||
|
||||
type (
|
||||
UpdateFunc func(src *domain.File) (*domain.File, error)
|
||||
|
||||
Repository interface {
|
||||
// Create save provided file into the store as a new media.
|
||||
// Returns error if media already exists.
|
||||
Create(ctx context.Context, path string, file domain.File) error
|
||||
|
||||
// Get returns a early stored media as a file. Returns error if
|
||||
// file is not exist.
|
||||
Get(ctx context.Context, path string) (*domain.File, error)
|
||||
|
||||
// Update replaces already exists media file or creates a new
|
||||
// one if it is not. Returns error overwise.
|
||||
Update(ctx context.Context, path string, update UpdateFunc) error
|
||||
|
||||
// Delete removes media file from the store. Returns error if
|
||||
// existed file cannot be or already deleted.
|
||||
Delete(ctx context.Context, path string) error
|
||||
}
|
||||
|
||||
dummyRepository struct{}
|
||||
|
||||
stubRepository struct {
|
||||
output *domain.File
|
||||
err error
|
||||
}
|
||||
|
||||
spyRepository struct {
|
||||
subRepository Repository
|
||||
Calls int
|
||||
Creates int
|
||||
Updates int
|
||||
Gets int
|
||||
Deletes int
|
||||
}
|
||||
|
||||
// NOTE(toby3d): fakeRepository is already provided by memory sub-package.
|
||||
// NOTE(toby3d): mockRepository is complicated. Mocking too much is bad.
|
||||
)
|
||||
|
||||
var (
|
||||
ErrExist error = errors.New("this file already exist")
|
||||
ErrNotExist error = errors.New("this file is not exist")
|
||||
)
|
||||
|
||||
// NewDummyMediaRepository creates an empty repository to satisfy contracts.
|
||||
// It is used in tests where repository working is not important.
|
||||
func NewDummyMediaRepository() Repository {
|
||||
return &dummyRepository{}
|
||||
}
|
||||
|
||||
func (dummyRepository) Create(_ context.Context, _ string, _ domain.File) error { return nil }
|
||||
func (dummyRepository) Get(_ context.Context, _ string) (*domain.File, error) { return nil, nil }
|
||||
func (dummyRepository) Update(_ context.Context, _ string, _ UpdateFunc) error { return nil }
|
||||
func (dummyRepository) Delete(_ context.Context, _ string) error { return nil }
|
||||
|
||||
// NewStubMediaRepository creates a repository that always returns input as a
|
||||
// output. It is used in tests where some dependency on the repository is
|
||||
// required.
|
||||
func NewStubMediaRepository(output *domain.File, err error) Repository {
|
||||
return &stubRepository{
|
||||
output: output,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *stubRepository) Create(_ context.Context, _ string, _ domain.File) error {
|
||||
return repo.err
|
||||
}
|
||||
|
||||
func (repo *stubRepository) Get(_ context.Context, _ string) (*domain.File, error) {
|
||||
return repo.output, repo.err
|
||||
}
|
||||
|
||||
func (repo *stubRepository) Update(_ context.Context, _ string, _ UpdateFunc) error {
|
||||
return repo.err
|
||||
}
|
||||
|
||||
func (repo *stubRepository) Delete(_ context.Context, _ string) error {
|
||||
return repo.err
|
||||
}
|
||||
|
||||
// NewSpyMediaRepository creates a spy repository which count outside calls,
|
||||
// based on provided subRepo. If subRepo is nil, then DummyRepository will be
|
||||
// used.
|
||||
func NewSpyMediaRepository(subRepo Repository) *spyRepository {
|
||||
if subRepo == nil {
|
||||
subRepo = NewDummyMediaRepository()
|
||||
}
|
||||
|
||||
return &spyRepository{
|
||||
subRepository: subRepo,
|
||||
Creates: 0,
|
||||
Updates: 0,
|
||||
Gets: 0,
|
||||
Deletes: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *spyRepository) Create(_ context.Context, _ string, _ domain.File) error {
|
||||
repo.Creates++
|
||||
|
||||
return repo.subRepository.Create(context.TODO(), "", domain.File{})
|
||||
}
|
||||
|
||||
func (repo *spyRepository) Get(_ context.Context, _ string) (*domain.File, error) {
|
||||
repo.Gets++
|
||||
|
||||
return repo.subRepository.Get(context.TODO(), "")
|
||||
}
|
||||
|
||||
func (repo *spyRepository) Update(_ context.Context, _ string, _ UpdateFunc) error {
|
||||
repo.Updates++
|
||||
|
||||
return repo.subRepository.Update(context.TODO(), "", nil)
|
||||
}
|
||||
|
||||
func (repo *spyRepository) Delete(_ context.Context, _ string) error {
|
||||
repo.Deletes++
|
||||
|
||||
return repo.subRepository.Delete(context.TODO(), "")
|
||||
}
|
84
internal/media/repository/memory/memory_media.go
Normal file
84
internal/media/repository/memory/memory_media.go
Normal file
|
@ -0,0 +1,84 @@
|
|||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"source.toby3d.me/toby3d/pub/internal/domain"
|
||||
"source.toby3d.me/toby3d/pub/internal/media"
|
||||
)
|
||||
|
||||
type memoryMediaRepository struct {
|
||||
mutex *sync.RWMutex
|
||||
media map[string]domain.File
|
||||
}
|
||||
|
||||
func NewMemoryMediaRepository() media.Repository {
|
||||
return &memoryMediaRepository{
|
||||
mutex: new(sync.RWMutex),
|
||||
media: make(map[string]domain.File),
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *memoryMediaRepository) Create(ctx context.Context, p string, f domain.File) error {
|
||||
p = path.Clean(strings.ToLower(p))
|
||||
|
||||
_, err := repo.Get(ctx, p)
|
||||
if err != nil && !errors.Is(err, media.ErrNotExist) {
|
||||
return fmt.Errorf("cannot save a new media: %w", err)
|
||||
}
|
||||
if err == nil {
|
||||
return media.ErrExist
|
||||
}
|
||||
|
||||
repo.mutex.Lock()
|
||||
defer repo.mutex.Unlock()
|
||||
|
||||
repo.media[p] = f
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *memoryMediaRepository) Get(ctx context.Context, p string) (*domain.File, error) {
|
||||
repo.mutex.RLock()
|
||||
defer repo.mutex.RUnlock()
|
||||
|
||||
if out, ok := repo.media[path.Clean(strings.ToLower(p))]; ok {
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
return nil, media.ErrNotExist
|
||||
}
|
||||
|
||||
func (repo *memoryMediaRepository) Update(ctx context.Context, p string, update media.UpdateFunc) error {
|
||||
p = path.Clean(strings.ToLower(p))
|
||||
|
||||
repo.mutex.Lock()
|
||||
defer repo.mutex.Unlock()
|
||||
|
||||
if in, ok := repo.media[p]; ok {
|
||||
out, err := update(&in)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update media: %w", err)
|
||||
}
|
||||
|
||||
repo.media[p] = *out
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return media.ErrNotExist
|
||||
}
|
||||
|
||||
func (repo *memoryMediaRepository) Delete(ctx context.Context, p string) error {
|
||||
repo.mutex.Lock()
|
||||
defer repo.mutex.Unlock()
|
||||
|
||||
delete(repo.media, path.Clean(strings.ToLower(p)))
|
||||
|
||||
return nil
|
||||
}
|
52
internal/media/usecase.go
Normal file
52
internal/media/usecase.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
package media
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
"source.toby3d.me/toby3d/pub/internal/domain"
|
||||
)
|
||||
|
||||
type (
|
||||
UseCase interface {
|
||||
// Upload uploads media file into micropub store which can be
|
||||
// download later.
|
||||
Upload(ctx context.Context, file domain.File) (*url.URL, error)
|
||||
|
||||
// Download downloads early uploaded media stored in path.
|
||||
Download(ctx context.Context, path string) (*domain.File, error)
|
||||
}
|
||||
|
||||
dummyUseCase struct{}
|
||||
|
||||
stubUseCase struct {
|
||||
u *url.URL
|
||||
f *domain.File
|
||||
err error
|
||||
}
|
||||
)
|
||||
|
||||
// NewDummyUseCase creates a dummy use case what does nothing.
|
||||
func NewDummyUseCase() UseCase {
|
||||
return &dummyUseCase{}
|
||||
}
|
||||
|
||||
func (dummyUseCase) Upload(_ context.Context, _ domain.File) (*url.URL, error) { return nil, nil }
|
||||
func (dummyUseCase) Download(_ context.Context, _ string) (*domain.File, error) { return nil, nil }
|
||||
|
||||
// NewDummyUseCase creates a stub use case what always returns provided input.
|
||||
func NewStubUseCase(u *url.URL, f *domain.File, err error) UseCase {
|
||||
return &stubUseCase{
|
||||
u: u,
|
||||
f: f,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
func (ucase stubUseCase) Upload(_ context.Context, _ domain.File) (*url.URL, error) {
|
||||
return ucase.u, ucase.err
|
||||
}
|
||||
|
||||
func (ucase stubUseCase) Download(_ context.Context, _ string) (*domain.File, error) {
|
||||
return ucase.f, ucase.err
|
||||
}
|
50
internal/media/usecase/media_ucase.go
Normal file
50
internal/media/usecase/media_ucase.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
|
||||
"source.toby3d.me/toby3d/pub/internal/domain"
|
||||
"source.toby3d.me/toby3d/pub/internal/media"
|
||||
)
|
||||
|
||||
type mediaUseCase struct {
|
||||
media media.Repository
|
||||
}
|
||||
|
||||
const charset string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
|
||||
|
||||
func NewMediaUseCase(media media.Repository) media.UseCase {
|
||||
return &mediaUseCase{
|
||||
media: media,
|
||||
}
|
||||
}
|
||||
|
||||
func (ucase *mediaUseCase) Upload(ctx context.Context, file domain.File) (*url.URL, error) {
|
||||
randName := make([]byte, 64)
|
||||
|
||||
for i := range randName {
|
||||
randName[i] = charset[rand.Intn(len(charset))]
|
||||
}
|
||||
|
||||
newName := string(randName) + "." + file.Ext()
|
||||
|
||||
if err := ucase.media.Create(ctx, newName, file); err != nil {
|
||||
return nil, fmt.Errorf("cannot upload nedia: %w", err)
|
||||
}
|
||||
|
||||
return &url.URL{
|
||||
Path: newName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ucase *mediaUseCase) Download(ctx context.Context, path string) (*domain.File, error) {
|
||||
out, err := ucase.media.Get(ctx, path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot find media file: %w", err)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
49
internal/media/usecase/media_ucase_test.go
Normal file
49
internal/media/usecase/media_ucase_test.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package usecase_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"source.toby3d.me/toby3d/pub/internal/domain"
|
||||
"source.toby3d.me/toby3d/pub/internal/media"
|
||||
"source.toby3d.me/toby3d/pub/internal/media/usecase"
|
||||
)
|
||||
|
||||
func TestUpload(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := domain.TestFile(t)
|
||||
repo := media.NewSpyMediaRepository(media.NewStubMediaRepository(f, nil))
|
||||
|
||||
out, err := usecase.NewMediaUseCase(repo).Upload(context.Background(), *f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if out.Path == "" {
|
||||
t.Error("expect non-empty location path, got nothing")
|
||||
}
|
||||
|
||||
if expect := 1; repo.Creates != expect {
|
||||
t.Errorf("expect %d Create calls, got %d", expect, repo.Creates)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownload(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := domain.TestFile(t)
|
||||
repo := media.NewStubMediaRepository(f, nil)
|
||||
|
||||
out, err := usecase.NewMediaUseCase(repo).
|
||||
Download(context.Background(), f.Path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(f, out); diff != "" {
|
||||
t.Errorf("%#+v", diff)
|
||||
}
|
||||
}
|
27
vendor/github.com/google/go-cmp/LICENSE
generated
vendored
Normal file
27
vendor/github.com/google/go-cmp/LICENSE
generated
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
Copyright (c) 2017 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
669
vendor/github.com/google/go-cmp/cmp/compare.go
generated
vendored
Normal file
669
vendor/github.com/google/go-cmp/cmp/compare.go
generated
vendored
Normal file
|
@ -0,0 +1,669 @@
|
|||
// Copyright 2017, The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package cmp determines equality of values.
|
||||
//
|
||||
// This package is intended to be a more powerful and safer alternative to
|
||||
// reflect.DeepEqual for comparing whether two values are semantically equal.
|
||||
// It is intended to only be used in tests, as performance is not a goal and
|
||||
// it may panic if it cannot compare the values. Its propensity towards
|
||||
// panicking means that its unsuitable for production environments where a
|
||||
// spurious panic may be fatal.
|
||||
//
|
||||
// The primary features of cmp are:
|
||||
//
|
||||
// - When the default behavior of equality does not suit the test's needs,
|
||||
// custom equality functions can override the equality operation.
|
||||
// For example, an equality function may report floats as equal so long as
|
||||
// they are within some tolerance of each other.
|
||||
//
|
||||
// - Types with an Equal method may use that method to determine equality.
|
||||
// This allows package authors to determine the equality operation
|
||||
// for the types that they define.
|
||||
//
|
||||
// - If no custom equality functions are used and no Equal method is defined,
|
||||
// equality is determined by recursively comparing the primitive kinds on
|
||||
// both values, much like reflect.DeepEqual. Unlike reflect.DeepEqual,
|
||||
// unexported fields are not compared by default; they result in panics
|
||||
// unless suppressed by using an Ignore option (see cmpopts.IgnoreUnexported)
|
||||
// or explicitly compared using the Exporter option.
|
||||
package cmp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-cmp/cmp/internal/diff"
|
||||
"github.com/google/go-cmp/cmp/internal/function"
|
||||
"github.com/google/go-cmp/cmp/internal/value"
|
||||
)
|
||||
|
||||
// TODO(≥go1.18): Use any instead of interface{}.
|
||||
|
||||
// Equal reports whether x and y are equal by recursively applying the
|
||||
// following rules in the given order to x and y and all of their sub-values:
|
||||
//
|
||||
// - Let S be the set of all Ignore, Transformer, and Comparer options that
|
||||
// remain after applying all path filters, value filters, and type filters.
|
||||
// If at least one Ignore exists in S, then the comparison is ignored.
|
||||
// If the number of Transformer and Comparer options in S is non-zero,
|
||||
// then Equal panics because it is ambiguous which option to use.
|
||||
// If S contains a single Transformer, then use that to transform
|
||||
// the current values and recursively call Equal on the output values.
|
||||
// If S contains a single Comparer, then use that to compare the current values.
|
||||
// Otherwise, evaluation proceeds to the next rule.
|
||||
//
|
||||
// - If the values have an Equal method of the form "(T) Equal(T) bool" or
|
||||
// "(T) Equal(I) bool" where T is assignable to I, then use the result of
|
||||
// x.Equal(y) even if x or y is nil. Otherwise, no such method exists and
|
||||
// evaluation proceeds to the next rule.
|
||||
//
|
||||
// - Lastly, try to compare x and y based on their basic kinds.
|
||||
// Simple kinds like booleans, integers, floats, complex numbers, strings,
|
||||
// and channels are compared using the equivalent of the == operator in Go.
|
||||
// Functions are only equal if they are both nil, otherwise they are unequal.
|
||||
//
|
||||
// Structs are equal if recursively calling Equal on all fields report equal.
|
||||
// If a struct contains unexported fields, Equal panics unless an Ignore option
|
||||
// (e.g., cmpopts.IgnoreUnexported) ignores that field or the Exporter option
|
||||
// explicitly permits comparing the unexported field.
|
||||
//
|
||||
// Slices are equal if they are both nil or both non-nil, where recursively
|
||||
// calling Equal on all non-ignored slice or array elements report equal.
|
||||
// Empty non-nil slices and nil slices are not equal; to equate empty slices,
|
||||
// consider using cmpopts.EquateEmpty.
|
||||
//
|
||||
// Maps are equal if they are both nil or both non-nil, where recursively
|
||||
// calling Equal on all non-ignored map entries report equal.
|
||||
// Map keys are equal according to the == operator.
|
||||
// To use custom comparisons for map keys, consider using cmpopts.SortMaps.
|
||||
// Empty non-nil maps and nil maps are not equal; to equate empty maps,
|
||||
// consider using cmpopts.EquateEmpty.
|
||||
//
|
||||
// Pointers and interfaces are equal if they are both nil or both non-nil,
|
||||
// where they have the same underlying concrete type and recursively
|
||||
// calling Equal on the underlying values reports equal.
|
||||
//
|
||||
// Before recursing into a pointer, slice element, or map, the current path
|
||||
// is checked to detect whether the address has already been visited.
|
||||
// If there is a cycle, then the pointed at values are considered equal
|
||||
// only if both addresses were previously visited in the same path step.
|
||||
func Equal(x, y interface{}, opts ...Option) bool {
|
||||
s := newState(opts)
|
||||
s.compareAny(rootStep(x, y))
|
||||
return s.result.Equal()
|
||||
}
|
||||
|
||||
// Diff returns a human-readable report of the differences between two values:
|
||||
// y - x. It returns an empty string if and only if Equal returns true for the
|
||||
// same input values and options.
|
||||
//
|
||||
// The output is displayed as a literal in pseudo-Go syntax.
|
||||
// At the start of each line, a "-" prefix indicates an element removed from x,
|
||||
// a "+" prefix to indicates an element added from y, and the lack of a prefix
|
||||
// indicates an element common to both x and y. If possible, the output
|
||||
// uses fmt.Stringer.String or error.Error methods to produce more humanly
|
||||
// readable outputs. In such cases, the string is prefixed with either an
|
||||
// 's' or 'e' character, respectively, to indicate that the method was called.
|
||||
//
|
||||
// Do not depend on this output being stable. If you need the ability to
|
||||
// programmatically interpret the difference, consider using a custom Reporter.
|
||||
func Diff(x, y interface{}, opts ...Option) string {
|
||||
s := newState(opts)
|
||||
|
||||
// Optimization: If there are no other reporters, we can optimize for the
|
||||
// common case where the result is equal (and thus no reported difference).
|
||||
// This avoids the expensive construction of a difference tree.
|
||||
if len(s.reporters) == 0 {
|
||||
s.compareAny(rootStep(x, y))
|
||||
if s.result.Equal() {
|
||||
return ""
|
||||
}
|
||||
s.result = diff.Result{} // Reset results
|
||||
}
|
||||
|
||||
r := new(defaultReporter)
|
||||
s.reporters = append(s.reporters, reporter{r})
|
||||
s.compareAny(rootStep(x, y))
|
||||
d := r.String()
|
||||
if (d == "") != s.result.Equal() {
|
||||
panic("inconsistent difference and equality results")
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// rootStep constructs the first path step. If x and y have differing types,
|
||||
// then they are stored within an empty interface type.
|
||||
func rootStep(x, y interface{}) PathStep {
|
||||
vx := reflect.ValueOf(x)
|
||||
vy := reflect.ValueOf(y)
|
||||
|
||||
// If the inputs are different types, auto-wrap them in an empty interface
|
||||
// so that they have the same parent type.
|
||||
var t reflect.Type
|
||||
if !vx.IsValid() || !vy.IsValid() || vx.Type() != vy.Type() {
|
||||
t = anyType
|
||||
if vx.IsValid() {
|
||||
vvx := reflect.New(t).Elem()
|
||||
vvx.Set(vx)
|
||||
vx = vvx
|
||||
}
|
||||
if vy.IsValid() {
|
||||
vvy := reflect.New(t).Elem()
|
||||
vvy.Set(vy)
|
||||
vy = vvy
|
||||
}
|
||||
} else {
|
||||
t = vx.Type()
|
||||
}
|
||||
|
||||
return &pathStep{t, vx, vy}
|
||||
}
|
||||
|
||||
type state struct {
|
||||
// These fields represent the "comparison state".
|
||||
// Calling statelessCompare must not result in observable changes to these.
|
||||
result diff.Result // The current result of comparison
|
||||
curPath Path // The current path in the value tree
|
||||
curPtrs pointerPath // The current set of visited pointers
|
||||
reporters []reporter // Optional reporters
|
||||
|
||||
// recChecker checks for infinite cycles applying the same set of
|
||||
// transformers upon the output of itself.
|
||||
recChecker recChecker
|
||||
|
||||
// dynChecker triggers pseudo-random checks for option correctness.
|
||||
// It is safe for statelessCompare to mutate this value.
|
||||
dynChecker dynChecker
|
||||
|
||||
// These fields, once set by processOption, will not change.
|
||||
exporters []exporter // List of exporters for structs with unexported fields
|
||||
opts Options // List of all fundamental and filter options
|
||||
}
|
||||
|
||||
func newState(opts []Option) *state {
|
||||
// Always ensure a validator option exists to validate the inputs.
|
||||
s := &state{opts: Options{validator{}}}
|
||||
s.curPtrs.Init()
|
||||
s.processOption(Options(opts))
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *state) processOption(opt Option) {
|
||||
switch opt := opt.(type) {
|
||||
case nil:
|
||||
case Options:
|
||||
for _, o := range opt {
|
||||
s.processOption(o)
|
||||
}
|
||||
case coreOption:
|
||||
type filtered interface {
|
||||
isFiltered() bool
|
||||
}
|
||||
if fopt, ok := opt.(filtered); ok && !fopt.isFiltered() {
|
||||
panic(fmt.Sprintf("cannot use an unfiltered option: %v", opt))
|
||||
}
|
||||
s.opts = append(s.opts, opt)
|
||||
case exporter:
|
||||
s.exporters = append(s.exporters, opt)
|
||||
case reporter:
|
||||
s.reporters = append(s.reporters, opt)
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown option %T", opt))
|
||||
}
|
||||
}
|
||||
|
||||
// statelessCompare compares two values and returns the result.
|
||||
// This function is stateless in that it does not alter the current result,
|
||||
// or output to any registered reporters.
|
||||
func (s *state) statelessCompare(step PathStep) diff.Result {
|
||||
// We do not save and restore curPath and curPtrs because all of the
|
||||
// compareX methods should properly push and pop from them.
|
||||
// It is an implementation bug if the contents of the paths differ from
|
||||
// when calling this function to when returning from it.
|
||||
|
||||
oldResult, oldReporters := s.result, s.reporters
|
||||
s.result = diff.Result{} // Reset result
|
||||
s.reporters = nil // Remove reporters to avoid spurious printouts
|
||||
s.compareAny(step)
|
||||
res := s.result
|
||||
s.result, s.reporters = oldResult, oldReporters
|
||||
return res
|
||||
}
|
||||
|
||||
func (s *state) compareAny(step PathStep) {
|
||||
// Update the path stack.
|
||||
s.curPath.push(step)
|
||||
defer s.curPath.pop()
|
||||
for _, r := range s.reporters {
|
||||
r.PushStep(step)
|
||||
defer r.PopStep()
|
||||
}
|
||||
s.recChecker.Check(s.curPath)
|
||||
|
||||
// Cycle-detection for slice elements (see NOTE in compareSlice).
|
||||
t := step.Type()
|
||||
vx, vy := step.Values()
|
||||
if si, ok := step.(SliceIndex); ok && si.isSlice && vx.IsValid() && vy.IsValid() {
|
||||
px, py := vx.Addr(), vy.Addr()
|
||||
if eq, visited := s.curPtrs.Push(px, py); visited {
|
||||
s.report(eq, reportByCycle)
|
||||
return
|
||||
}
|
||||
defer s.curPtrs.Pop(px, py)
|
||||
}
|
||||
|
||||
// Rule 1: Check whether an option applies on this node in the value tree.
|
||||
if s.tryOptions(t, vx, vy) {
|
||||
return
|
||||
}
|
||||
|
||||
// Rule 2: Check whether the type has a valid Equal method.
|
||||
if s.tryMethod(t, vx, vy) {
|
||||
return
|
||||
}
|
||||
|
||||
// Rule 3: Compare based on the underlying kind.
|
||||
switch t.Kind() {
|
||||
case reflect.Bool:
|
||||
s.report(vx.Bool() == vy.Bool(), 0)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
s.report(vx.Int() == vy.Int(), 0)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
s.report(vx.Uint() == vy.Uint(), 0)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
s.report(vx.Float() == vy.Float(), 0)
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
s.report(vx.Complex() == vy.Complex(), 0)
|
||||
case reflect.String:
|
||||
s.report(vx.String() == vy.String(), 0)
|
||||
case reflect.Chan, reflect.UnsafePointer:
|
||||
s.report(vx.Pointer() == vy.Pointer(), 0)
|
||||
case reflect.Func:
|
||||
s.report(vx.IsNil() && vy.IsNil(), 0)
|
||||
case reflect.Struct:
|
||||
s.compareStruct(t, vx, vy)
|
||||
case reflect.Slice, reflect.Array:
|
||||
s.compareSlice(t, vx, vy)
|
||||
case reflect.Map:
|
||||
s.compareMap(t, vx, vy)
|
||||
case reflect.Ptr:
|
||||
s.comparePtr(t, vx, vy)
|
||||
case reflect.Interface:
|
||||
s.compareInterface(t, vx, vy)
|
||||
default:
|
||||
panic(fmt.Sprintf("%v kind not handled", t.Kind()))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *state) tryOptions(t reflect.Type, vx, vy reflect.Value) bool {
|
||||
// Evaluate all filters and apply the remaining options.
|
||||
if opt := s.opts.filter(s, t, vx, vy); opt != nil {
|
||||
opt.apply(s, vx, vy)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *state) tryMethod(t reflect.Type, vx, vy reflect.Value) bool {
|
||||
// Check if this type even has an Equal method.
|
||||
m, ok := t.MethodByName("Equal")
|
||||
if !ok || !function.IsType(m.Type, function.EqualAssignable) {
|
||||
return false
|
||||
}
|
||||
|
||||
eq := s.callTTBFunc(m.Func, vx, vy)
|
||||
s.report(eq, reportByMethod)
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *state) callTRFunc(f, v reflect.Value, step Transform) reflect.Value {
|
||||
if !s.dynChecker.Next() {
|
||||
return f.Call([]reflect.Value{v})[0]
|
||||
}
|
||||
|
||||
// Run the function twice and ensure that we get the same results back.
|
||||
// We run in goroutines so that the race detector (if enabled) can detect
|
||||
// unsafe mutations to the input.
|
||||
c := make(chan reflect.Value)
|
||||
go detectRaces(c, f, v)
|
||||
got := <-c
|
||||
want := f.Call([]reflect.Value{v})[0]
|
||||
if step.vx, step.vy = got, want; !s.statelessCompare(step).Equal() {
|
||||
// To avoid false-positives with non-reflexive equality operations,
|
||||
// we sanity check whether a value is equal to itself.
|
||||
if step.vx, step.vy = want, want; !s.statelessCompare(step).Equal() {
|
||||
return want
|
||||
}
|
||||
panic(fmt.Sprintf("non-deterministic function detected: %s", function.NameOf(f)))
|
||||
}
|
||||
return want
|
||||
}
|
||||
|
||||
func (s *state) callTTBFunc(f, x, y reflect.Value) bool {
|
||||
if !s.dynChecker.Next() {
|
||||
return f.Call([]reflect.Value{x, y})[0].Bool()
|
||||
}
|
||||
|
||||
// Swapping the input arguments is sufficient to check that
|
||||
// f is symmetric and deterministic.
|
||||
// We run in goroutines so that the race detector (if enabled) can detect
|
||||
// unsafe mutations to the input.
|
||||
c := make(chan reflect.Value)
|
||||
go detectRaces(c, f, y, x)
|
||||
got := <-c
|
||||
want := f.Call([]reflect.Value{x, y})[0].Bool()
|
||||
if !got.IsValid() || got.Bool() != want {
|
||||
panic(fmt.Sprintf("non-deterministic or non-symmetric function detected: %s", function.NameOf(f)))
|
||||
}
|
||||
return want
|
||||
}
|
||||
|
||||
func detectRaces(c chan<- reflect.Value, f reflect.Value, vs ...reflect.Value) {
|
||||
var ret reflect.Value
|
||||
defer func() {
|
||||
recover() // Ignore panics, let the other call to f panic instead
|
||||
c <- ret
|
||||
}()
|
||||
ret = f.Call(vs)[0]
|
||||
}
|
||||
|
||||
func (s *state) compareStruct(t reflect.Type, vx, vy reflect.Value) {
|
||||
var addr bool
|
||||
var vax, vay reflect.Value // Addressable versions of vx and vy
|
||||
|
||||
var mayForce, mayForceInit bool
|
||||
step := StructField{&structField{}}
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
step.typ = t.Field(i).Type
|
||||
step.vx = vx.Field(i)
|
||||
step.vy = vy.Field(i)
|
||||
step.name = t.Field(i).Name
|
||||
step.idx = i
|
||||
step.unexported = !isExported(step.name)
|
||||
if step.unexported {
|
||||
if step.name == "_" {
|
||||
continue
|
||||
}
|
||||
// Defer checking of unexported fields until later to give an
|
||||
// Ignore a chance to ignore the field.
|
||||
if !vax.IsValid() || !vay.IsValid() {
|
||||
// For retrieveUnexportedField to work, the parent struct must
|
||||
// be addressable. Create a new copy of the values if
|
||||
// necessary to make them addressable.
|
||||
addr = vx.CanAddr() || vy.CanAddr()
|
||||
vax = makeAddressable(vx)
|
||||
vay = makeAddressable(vy)
|
||||
}
|
||||
if !mayForceInit {
|
||||
for _, xf := range s.exporters {
|
||||
mayForce = mayForce || xf(t)
|
||||
}
|
||||
mayForceInit = true
|
||||
}
|
||||
step.mayForce = mayForce
|
||||
step.paddr = addr
|
||||
step.pvx = vax
|
||||
step.pvy = vay
|
||||
step.field = t.Field(i)
|
||||
}
|
||||
s.compareAny(step)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *state) compareSlice(t reflect.Type, vx, vy reflect.Value) {
|
||||
isSlice := t.Kind() == reflect.Slice
|
||||
if isSlice && (vx.IsNil() || vy.IsNil()) {
|
||||
s.report(vx.IsNil() && vy.IsNil(), 0)
|
||||
return
|
||||
}
|
||||
|
||||
// NOTE: It is incorrect to call curPtrs.Push on the slice header pointer
|
||||
// since slices represents a list of pointers, rather than a single pointer.
|
||||
// The pointer checking logic must be handled on a per-element basis
|
||||
// in compareAny.
|
||||
//
|
||||
// A slice header (see reflect.SliceHeader) in Go is a tuple of a starting
|
||||
// pointer P, a length N, and a capacity C. Supposing each slice element has
|
||||
// a memory size of M, then the slice is equivalent to the list of pointers:
|
||||
// [P+i*M for i in range(N)]
|
||||
//
|
||||
// For example, v[:0] and v[:1] are slices with the same starting pointer,
|
||||
// but they are clearly different values. Using the slice pointer alone
|
||||
// violates the assumption that equal pointers implies equal values.
|
||||
|
||||
step := SliceIndex{&sliceIndex{pathStep: pathStep{typ: t.Elem()}, isSlice: isSlice}}
|
||||
withIndexes := func(ix, iy int) SliceIndex {
|
||||
if ix >= 0 {
|
||||
step.vx, step.xkey = vx.Index(ix), ix
|
||||
} else {
|
||||
step.vx, step.xkey = reflect.Value{}, -1
|
||||
}
|
||||
if iy >= 0 {
|
||||
step.vy, step.ykey = vy.Index(iy), iy
|
||||
} else {
|
||||
step.vy, step.ykey = reflect.Value{}, -1
|
||||
}
|
||||
return step
|
||||
}
|
||||
|
||||
// Ignore options are able to ignore missing elements in a slice.
|
||||
// However, detecting these reliably requires an optimal differencing
|
||||
// algorithm, for which diff.Difference is not.
|
||||
//
|
||||
// Instead, we first iterate through both slices to detect which elements
|
||||
// would be ignored if standing alone. The index of non-discarded elements
|
||||
// are stored in a separate slice, which diffing is then performed on.
|
||||
var indexesX, indexesY []int
|
||||
var ignoredX, ignoredY []bool
|
||||
for ix := 0; ix < vx.Len(); ix++ {
|
||||
ignored := s.statelessCompare(withIndexes(ix, -1)).NumDiff == 0
|
||||
if !ignored {
|
||||
indexesX = append(indexesX, ix)
|
||||
}
|
||||
ignoredX = append(ignoredX, ignored)
|
||||
}
|
||||
for iy := 0; iy < vy.Len(); iy++ {
|
||||
ignored := s.statelessCompare(withIndexes(-1, iy)).NumDiff == 0
|
||||
if !ignored {
|
||||
indexesY = append(indexesY, iy)
|
||||
}
|
||||
ignoredY = append(ignoredY, ignored)
|
||||
}
|
||||
|
||||
// Compute an edit-script for slices vx and vy (excluding ignored elements).
|
||||
edits := diff.Difference(len(indexesX), len(indexesY), func(ix, iy int) diff.Result {
|
||||
return s.statelessCompare(withIndexes(indexesX[ix], indexesY[iy]))
|
||||
})
|
||||
|
||||
// Replay the ignore-scripts and the edit-script.
|
||||
var ix, iy int
|
||||
for ix < vx.Len() || iy < vy.Len() {
|
||||
var e diff.EditType
|
||||
switch {
|
||||
case ix < len(ignoredX) && ignoredX[ix]:
|
||||
e = diff.UniqueX
|
||||
case iy < len(ignoredY) && ignoredY[iy]:
|
||||
e = diff.UniqueY
|
||||
default:
|
||||
e, edits = edits[0], edits[1:]
|
||||
}
|
||||
switch e {
|
||||
case diff.UniqueX:
|
||||
s.compareAny(withIndexes(ix, -1))
|
||||
ix++
|
||||
case diff.UniqueY:
|
||||
s.compareAny(withIndexes(-1, iy))
|
||||
iy++
|
||||
default:
|
||||
s.compareAny(withIndexes(ix, iy))
|
||||
ix++
|
||||
iy++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *state) compareMap(t reflect.Type, vx, vy reflect.Value) {
|
||||
if vx.IsNil() || vy.IsNil() {
|
||||
s.report(vx.IsNil() && vy.IsNil(), 0)
|
||||
return
|
||||
}
|
||||
|
||||
// Cycle-detection for maps.
|
||||
if eq, visited := s.curPtrs.Push(vx, vy); visited {
|
||||
s.report(eq, reportByCycle)
|
||||
return
|
||||
}
|
||||
defer s.curPtrs.Pop(vx, vy)
|
||||
|
||||
// We combine and sort the two map keys so that we can perform the
|
||||
// comparisons in a deterministic order.
|
||||
step := MapIndex{&mapIndex{pathStep: pathStep{typ: t.Elem()}}}
|
||||
for _, k := range value.SortKeys(append(vx.MapKeys(), vy.MapKeys()...)) {
|
||||
step.vx = vx.MapIndex(k)
|
||||
step.vy = vy.MapIndex(k)
|
||||
step.key = k
|
||||
if !step.vx.IsValid() && !step.vy.IsValid() {
|
||||
// It is possible for both vx and vy to be invalid if the
|
||||
// key contained a NaN value in it.
|
||||
//
|
||||
// Even with the ability to retrieve NaN keys in Go 1.12,
|
||||
// there still isn't a sensible way to compare the values since
|
||||
// a NaN key may map to multiple unordered values.
|
||||
// The most reasonable way to compare NaNs would be to compare the
|
||||
// set of values. However, this is impossible to do efficiently
|
||||
// since set equality is provably an O(n^2) operation given only
|
||||
// an Equal function. If we had a Less function or Hash function,
|
||||
// this could be done in O(n*log(n)) or O(n), respectively.
|
||||
//
|
||||
// Rather than adding complex logic to deal with NaNs, make it
|
||||
// the user's responsibility to compare such obscure maps.
|
||||
const help = "consider providing a Comparer to compare the map"
|
||||
panic(fmt.Sprintf("%#v has map key with NaNs\n%s", s.curPath, help))
|
||||
}
|
||||
s.compareAny(step)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *state) comparePtr(t reflect.Type, vx, vy reflect.Value) {
|
||||
if vx.IsNil() || vy.IsNil() {
|
||||
s.report(vx.IsNil() && vy.IsNil(), 0)
|
||||
return
|
||||
}
|
||||
|
||||
// Cycle-detection for pointers.
|
||||
if eq, visited := s.curPtrs.Push(vx, vy); visited {
|
||||
s.report(eq, reportByCycle)
|
||||
return
|
||||
}
|
||||
defer s.curPtrs.Pop(vx, vy)
|
||||
|
||||
vx, vy = vx.Elem(), vy.Elem()
|
||||
s.compareAny(Indirect{&indirect{pathStep{t.Elem(), vx, vy}}})
|
||||
}
|
||||
|
||||
func (s *state) compareInterface(t reflect.Type, vx, vy reflect.Value) {
|
||||
if vx.IsNil() || vy.IsNil() {
|
||||
s.report(vx.IsNil() && vy.IsNil(), 0)
|
||||
return
|
||||
}
|
||||
vx, vy = vx.Elem(), vy.Elem()
|
||||
if vx.Type() != vy.Type() {
|
||||
s.report(false, 0)
|
||||
return
|
||||
}
|
||||
s.compareAny(TypeAssertion{&typeAssertion{pathStep{vx.Type(), vx, vy}}})
|
||||
}
|
||||
|
||||
func (s *state) report(eq bool, rf resultFlags) {
|
||||
if rf&reportByIgnore == 0 {
|
||||
if eq {
|
||||
s.result.NumSame++
|
||||
rf |= reportEqual
|
||||
} else {
|
||||
s.result.NumDiff++
|
||||
rf |= reportUnequal
|
||||
}
|
||||
}
|
||||
for _, r := range s.reporters {
|
||||
r.Report(Result{flags: rf})
|
||||
}
|
||||
}
|
||||
|
||||
// recChecker tracks the state needed to periodically perform checks that
|
||||
// user provided transformers are not stuck in an infinitely recursive cycle.
|
||||
type recChecker struct{ next int }
|
||||
|
||||
// Check scans the Path for any recursive transformers and panics when any
|
||||
// recursive transformers are detected. Note that the presence of a
|
||||
// recursive Transformer does not necessarily imply an infinite cycle.
|
||||
// As such, this check only activates after some minimal number of path steps.
|
||||
func (rc *recChecker) Check(p Path) {
|
||||
const minLen = 1 << 16
|
||||
if rc.next == 0 {
|
||||
rc.next = minLen
|
||||
}
|
||||
if len(p) < rc.next {
|
||||
return
|
||||
}
|
||||
rc.next <<= 1
|
||||
|
||||
// Check whether the same transformer has appeared at least twice.
|
||||
var ss []string
|
||||
m := map[Option]int{}
|
||||
for _, ps := range p {
|
||||
if t, ok := ps.(Transform); ok {
|
||||
t := t.Option()
|
||||
if m[t] == 1 { // Transformer was used exactly once before
|
||||
tf := t.(*transformer).fnc.Type()
|
||||
ss = append(ss, fmt.Sprintf("%v: %v => %v", t, tf.In(0), tf.Out(0)))
|
||||
}
|
||||
m[t]++
|
||||
}
|
||||
}
|
||||
if len(ss) > 0 {
|
||||
const warning = "recursive set of Transformers detected"
|
||||
const help = "consider using cmpopts.AcyclicTransformer"
|
||||
set := strings.Join(ss, "\n\t")
|
||||
panic(fmt.Sprintf("%s:\n\t%s\n%s", warning, set, help))
|
||||
}
|
||||
}
|
||||
|
||||
// dynChecker tracks the state needed to periodically perform checks that
|
||||
// user provided functions are symmetric and deterministic.
|
||||
// The zero value is safe for immediate use.
|
||||
type dynChecker struct{ curr, next int }
|
||||
|
||||
// Next increments the state and reports whether a check should be performed.
|
||||
//
|
||||
// Checks occur every Nth function call, where N is a triangular number:
|
||||
//
|
||||
// 0 1 3 6 10 15 21 28 36 45 55 66 78 91 105 120 136 153 171 190 ...
|
||||
//
|
||||
// See https://en.wikipedia.org/wiki/Triangular_number
|
||||
//
|
||||
// This sequence ensures that the cost of checks drops significantly as
|
||||
// the number of functions calls grows larger.
|
||||
func (dc *dynChecker) Next() bool {
|
||||
ok := dc.curr == dc.next
|
||||
if ok {
|
||||
dc.curr = 0
|
||||
dc.next++
|
||||
}
|
||||
dc.curr++
|
||||
return ok
|
||||
}
|
||||
|
||||
// makeAddressable returns a value that is always addressable.
|
||||
// It returns the input verbatim if it is already addressable,
|
||||
// otherwise it creates a new value and returns an addressable copy.
|
||||
func makeAddressable(v reflect.Value) reflect.Value {
|
||||
if v.CanAddr() {
|
||||
return v
|
||||
}
|
||||
vc := reflect.New(v.Type()).Elem()
|
||||
vc.Set(v)
|
||||
return vc
|
||||
}
|
16
vendor/github.com/google/go-cmp/cmp/export_panic.go
generated
vendored
Normal file
16
vendor/github.com/google/go-cmp/cmp/export_panic.go
generated
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
// Copyright 2017, The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build purego
|
||||
// +build purego
|
||||
|
||||
package cmp
|
||||
|
||||